diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..f3245e76 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..30262b05 --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# Unused atm (jest sets this to `test`) +NODE_ENV=development + +# Debug node modules - https://nodejs.org/api/cli.html#node_debugmodule +# NODE_DEBUG= + +# Debug node native modules - https://nodejs.org/api/cli.html#node_debug_nativemodule +# NODE_DEBUG_NATIVE= + +# Path to PK executable to override tests/bin target +# PK_TEST_COMMAND= + +# If set, indicates that `PK_TEST_COMMAND` is targetting docker +# PK_TEST_COMMAND_DOCKER= +# Accessing AWS for testnet.polykey.io and mainnet.polykey.io deployment +AWS_DEFAULT_REGION='ap-southeast-2' +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# Path to container registry authentication file used by `skopeo` +# The file has the same contents as `DOCKER_AUTH_CONFIG` +# Use this command to acquire the auth file at `./tmp/auth.json`: +# ``` +# printf 'PASSWORD' | skopeo login \ +# --username 'USERNAME' \ +# --password-stdin \ +# $CI_REGISTRY_IMAGE \ +# --authfile=./tmp/auth.json +# ``` +# REGISTRY_AUTH_FILE= + +# Authenticate to GitHub with `gh` +# GITHUB_TOKEN= + +# To allow testing different executables in the bin tests +# Both PK_TEST_COMMAND and PK_TEST_PLATFORM must be set at the same time +# PK_TEST_COMMAND= #Specify the shell command we want to test against +# PK_TEST_PLATFORM=docker #Overrides the auto set `testPlatform` variable used for enabling platform specific tests +# PK_TEST_TMPDIR= #Sets the `global.tmpDir` variable to allow overriding the temp directory used for tests + diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..72f98eb5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +src/proto/* diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..1eec3982 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,177 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es2021": true, + "node": true, + "jest": true + }, + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "plugins": [ + "import" + ], + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "rules": { + "linebreak-style": ["error", "unix"], + "no-empty": 1, + "no-useless-catch": 1, + "no-prototype-builtins": 1, + "no-constant-condition": 0, + "no-useless-escape": 0, + "no-console": "error", + "no-restricted-globals": [ + "error", + { + "name": "global", + "message": "Use `globalThis` instead" + }, + { + "name": "window", + "message": "Use `globalThis` instead" + } + ], + "require-yield": 0, + "eqeqeq": ["error", "smart"], + "spaced-comment": [ + "warn", + "always", + { + "line": { + "exceptions": ["-"] + }, + "block": { + "exceptions": ["*"] + }, + "markers": ["/"] + } + ], + "capitalized-comments": [ + "warn", + "always", + { + "ignoreInlineComments": true, + "ignoreConsecutiveComments": true + } + ], + "curly": [ + "error", + "multi-line", + "consistent" + ], + "import/order": [ + "error", + { + "groups": [ + "type", + "builtin", + "external", + "internal", + "index", + "sibling", + "parent", + "object" + ], + "pathGroups": [ + { + "pattern": "@", + "group": "internal" + }, + { + "pattern": "@/**", + "group": "internal" + } + ], + "pathGroupsExcludedImportTypes": [ + "type" + ], + "newlines-between": "never" + } + ], + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "varsIgnorePattern": "^_", + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-inferrable-types": 0, + "@typescript-eslint/no-non-null-assertion": 0, + "@typescript-eslint/no-this-alias": 0, + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/consistent-type-imports": ["error"], + "@typescript-eslint/consistent-type-exports": ["error"], + "no-throw-literal": "off", + "@typescript-eslint/no-throw-literal": "off", + "@typescript-eslint/no-floating-promises": ["error", { + "ignoreVoid": true, + "ignoreIIFE": true + }], + "@typescript-eslint/no-misused-promises": ["error", { + "checksVoidReturn": false + }], + "@typescript-eslint/await-thenable": ["error"], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "default", + "format": ["camelCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "function", + "format": ["camelCase", "PascalCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "variable", + "format": ["camelCase", "UPPER_CASE", "PascalCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "parameter", + "format": ["camelCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "typeLike", + "format": ["PascalCase"], + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "enumMember", + "format": ["PascalCase", "UPPER_CASE"] + }, + { + "selector": "objectLiteralProperty", + "format": null + }, + { + "selector": "typeProperty", + "format": null + } + ], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-ignore": "allow-with-description" + } + ] + } +} diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml new file mode 100644 index 00000000..80f58e63 --- /dev/null +++ b/.github/workflows/codesee-arch-diagram.yml @@ -0,0 +1,23 @@ +# This workflow was added by CodeSee. Learn more at https://codesee.io/ +# This is v2.0 of this workflow file +on: + push: + branches: + - staging + pull_request_target: + types: [opened, synchronize, reopened] + +name: CodeSee + +permissions: read-all + +jobs: + codesee: + runs-on: ubuntu-latest + continue-on-error: true + name: Analyze the repo with CodeSee + steps: + - uses: Codesee-io/codesee-action@v2 + with: + codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} + codesee-url: https://app.codesee.io diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..78a1b31b --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +/tmp +/dist +.env* +!.env.example +# nix +/result* +/builds +# node-gyp +/build +# prebuildify +/prebuilds + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# editor +.vscode/ +.idea/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..f5a0d165 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,662 @@ +workflow: + rules: + # Disable merge request pipelines + - if: $CI_MERGE_REQUEST_ID + when: never + - when: always + +variables: + GIT_SUBMODULE_STRATEGY: recursive + GH_PROJECT_PATH: "MatrixAI/${CI_PROJECT_NAME}" + GH_PROJECT_URL: "https://${GITHUB_TOKEN}@github.com/${GH_PROJECT_PATH}.git" + # Cache .npm + npm_config_cache: "${CI_PROJECT_DIR}/tmp/npm" + # Prefer offline node module installation + npm_config_prefer_offline: "true" + # Homebrew cache only used by macos runner + HOMEBREW_CACHE: "${CI_PROJECT_DIR}/tmp/Homebrew" + +default: + image: registry.gitlab.com/matrixai/engineering/maintenance/gitlab-runner + interruptible: true + before_script: + # Replace this in windows runners that use powershell + # with `mkdir -Force "$CI_PROJECT_DIR/tmp"` + - mkdir -p "$CI_PROJECT_DIR/tmp" + +# Cached directories shared between jobs & pipelines per-branch per-runner +cache: + key: $CI_COMMIT_REF_SLUG + # Preserve cache even if job fails + when: 'always' + paths: + - ./tmp/npm/ + # Homebrew cache is only used by the macos runner + - ./tmp/Homebrew + # Chocolatey cache is only used by the windows runner + - ./tmp/chocolatey/ + # `jest` cache is configured in jest.config.js + - ./tmp/jest/ + +stages: + - check # Linting, unit tests + - build # Cross-platform library compilation, unit tests + - integration # Cross-platform application bundling, integration tests, and pre-release + - release # Cross-platform distribution and deployment + +check:scratch: + stage: check + needs: [] + script: + - > + nix-shell --arg ci true --run $' + npm test -- --ci tests/scratch.test.ts; + ' + allow_failure: true + rules: + - when: manual + +check:lint: + stage: check + needs: [] + script: + - > + nix-shell --arg ci true --run $' + npm run lint; + npm run lint-shell; + ' + rules: + # Runs on feature and staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH =~ /^(?:feature.*|staging)$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Manually run on commits other than master and ignore version commits + - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != 'master' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + when: manual + +check:nix-dry: + stage: check + needs: [] + script: + - nix-build -v -v --dry-run ./release.nix + rules: + # Runs on feature and staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH =~ /^(?:feature.*|staging)$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Manually run on commits other than master and ignore version commits + - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != 'master' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + when: manual + +check:test-generate: + stage: check + needs: [] + script: + - > + nix-shell --arg ci true --run $' + ./scripts/check-test-generate.sh > ./tmp/check-test.yml; + ' + artifacts: + when: always + paths: + - ./tmp/check-test.yml + rules: + # Runs on feature commits and ignores version commits + - if: $CI_COMMIT_BRANCH =~ /^feature.*$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Manually run on commits other than master and staging and ignore version commits + - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH !~ /^(?:master|staging)$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + when: manual + +check:test: + stage: check + needs: + - check:test-generate + trigger: + include: + - artifact: tmp/check-test.yml + job: check:test-generate + strategy: depend + inherit: + variables: false + variables: + PARENT_PIPELINE_ID: $CI_PIPELINE_ID + rules: + # Runs on feature commits and ignores version commits + - if: $CI_COMMIT_BRANCH =~ /^feature.*$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Manually run on commits other than master and staging and ignore version commits + - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH !~ /^(?:master|staging)$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + when: manual + +build:merge: + stage: build + needs: [] + allow_failure: true + script: + # Required for `gh pr create` + - git remote add upstream "$GH_PROJECT_URL" + - > + nix-shell --arg ci true --run $' + gh pr create \ + --head staging \ + --base master \ + --title "ci: merge staging to master" \ + --body "This is an automatic PR generated by the pipeline CI/CD. This will be automatically fast-forward merged if successful." \ + --assignee "@me" \ + --no-maintainer-edit \ + --repo "$GH_PROJECT_PATH" || true; + printf "Pipeline Attempt on ${CI_PIPELINE_ID} for ${CI_COMMIT_SHA}\n\n${CI_PIPELINE_URL}" \ + | gh pr comment staging \ + --body-file - \ + --repo "$GH_PROJECT_PATH"; + ' + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +build:dist: + stage: build + needs: [] + script: + - > + nix-shell --arg ci true --run $' + npm run build --verbose; + ' + artifacts: + when: always + paths: + - ./dist + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +build:platforms-generate: + stage: build + needs: [] + script: + - > + nix-shell --arg ci true --run $' + ./scripts/build-platforms-generate.sh > ./tmp/build-platforms.yml; + ' + artifacts: + when: always + paths: + - ./tmp/build-platforms.yml + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +build:platforms: + stage: build + needs: + - build:platforms-generate + trigger: + include: + - artifact: tmp/build-platforms.yml + job: build:platforms-generate + strategy: depend + inherit: + variables: false + variables: + PARENT_PIPELINE_ID: $CI_PIPELINE_ID + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +build:prerelease: + stage: build + needs: + - build:dist + - build:platforms + # Don't interrupt publishing job + interruptible: false + script: + - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ./.npmrc + - echo 'Publishing library prerelease' + - > + nix-shell --arg ci true --run $' + npm publish --tag prerelease --access public; + ' + after_script: + - rm -f ./.npmrc + rules: + # Only runs on tag pipeline where the tag is a prerelease version + # This requires dependencies to also run on tag pipeline + # However version tag comes with a version commit + # Dependencies must not run on the version commit + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+-.*[0-9]+$/ + +integration:builds: + stage: integration + needs: + - build:dist + - build:platforms + script: + - mkdir -p ./builds + - > + build_application="$(nix-build \ + --max-jobs "$(nproc)" --cores "$(nproc)" \ + ./release.nix \ + --attr application \ + )" + - > + nix-store --export $( \ + nix-store --query --requisites "$build_application" \ + ) | gzip > ./builds/js-polykey.closure.gz + # non-nix targets + - > + builds="$(nix-build \ + --max-jobs "$(nproc)" --cores "$(nproc)" \ + ./release.nix \ + --attr docker \ + --attr package.linux.x64.elf \ + --attr package.windows.x64.exe \ + --attr package.macos.x64.macho \ + --attr package.macos.arm64.macho)" + - cp -r $builds ./builds/ + artifacts: + paths: + - ./builds/ + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +integration:deployment: + stage: integration + needs: + - integration:builds + # Don't interrupt deploying job + interruptible: false + # Requires mutual exclusion + resource_group: integration:deployment + environment: + name: 'testnet' + deployment_tier: 'staging' + url: 'https://testnet.polykey.io' + variables: + REGISTRY_AUTH_FILE: "./tmp/registry-auth-file.json" + # Override CI_REGISTRY_IMAGE to point to ECR + CI_REGISTRY_IMAGE: '015248367786.dkr.ecr.ap-southeast-2.amazonaws.com/polykey' + script: + - echo 'Deploying container image to ECR' + - > + nix-shell --arg ci true --run $' + aws ecr get-login-password \ + | skopeo login \ + --username AWS \ + --password-stdin \ + --authfile "$REGISTRY_AUTH_FILE" \ + "$CI_REGISTRY_IMAGE"; + image=(./builds/*-docker-*); + ./scripts/deploy-image.sh "${image[0]}" \'testnet\' "$CI_REGISTRY_IMAGE"; + ' + - echo 'Deploying ECS service to testnet' + - > + nix-shell --run $' + ./scripts/deploy-service.sh \'polykey-testnet\'; + ' + after_script: + - rm -f "$REGISTRY_AUTH_FILE" + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +integration:nix: + stage: integration + needs: + - integration:builds + - job: integration:deployment + optional: true + script: + - > + build_application="$( \ + gunzip -c ./builds/js-polykey.closure.gz | \ + nix-store --import | \ + tail -1 \ + )" + - $build_application/polykey + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +integration:docker: + stage: integration + needs: + - integration:builds + - job: integration:deployment + optional: true + services: + - docker:20.10.16-dind + variables: + DOCKER_TLS_CERTDIR: "/certs" + FF_NETWORK_PER_BUILD: "true" + PK_TEST_PLATFORM: "docker" + PK_TEST_TMPDIR: "${CI_PROJECT_DIR}/tmp/test" + script: + - docker info + - mkdir $PK_TEST_TMPDIR + - > + nix-shell --arg ci true --run $' + image_and_tag="$(docker load --input ./builds/*docker* | cut -d\' \' -f3)"; + PK_TEST_COMMAND="docker run \$DOCKER_OPTIONS $image_and_tag" npm run test; + ' + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +integration:linux: + stage: integration + needs: + - integration:builds + - job: integration:deployment + optional: true + image: ubuntu:latest + script: + - for f in ./builds/*-linux-*; do "$f"; done + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +.integration:windows: + inherit: + default: + - interruptible + stage: integration + needs: + - integration:builds + - job: integration:deployment + optional: true + tags: + - windows + before_script: + - mkdir -Force "$CI_PROJECT_DIR/tmp" + script: + - Get-ChildItem -File ./builds/*-win-* | ForEach {& $_.FullName} + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +.integration:macos: + stage: integration + needs: + - integration:builds + - job: integration:deployment + optional: true + tags: + - saas-macos-medium-m1 + image: macos-12-xcode-14 + script: + - for f in ./builds/*-macos-x64*; do "$f"; done + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +integration:prerelease: + stage: integration + needs: + - integration:builds + - job: build:prerelease + optional: true + - job: integration:nix + optional: true + - job: integration:docker + optional: true + - job: integration:linux + optional: true + # - job: integration:windows + # optional: true + # - job: integration:macos + # optional: true + # Don't interrupt publishing job + interruptible: false + # Requires mutual exclusion + resource_group: integration:prerelease + variables: + REGISTRY_AUTH_FILE: "./tmp/registry-auth-file.json" + script: + - echo 'Publishing application prerelease' + - > + nix-shell --arg ci true --run $' + if gh release view "$CI_COMMIT_TAG" --repo "$GH_PROJECT_PATH" >/dev/null; then \ + gh release \ + upload "$CI_COMMIT_TAG" \ + builds/*.closure.gz \ + builds/*-docker-* \ + builds/*-linux-* \ + builds/*-win-* \ + builds/*-macos-* \ + --clobber \ + --repo "$GH_PROJECT_PATH"; \ + else \ + gh release \ + create "$CI_COMMIT_TAG" \ + builds/*.closure.gz \ + builds/*-docker-* \ + builds/*-linux-* \ + builds/*-win-* \ + builds/*-macos-* \ + --title "${CI_COMMIT_TAG}-$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ + --notes "" \ + --prerelease \ + --target staging \ + --repo "$GH_PROJECT_PATH"; \ + fi; + ' + - echo 'Prereleasing container image' + - > + nix-shell --arg ci true --run $' + skopeo login \ + --username "$CI_REGISTRY_USER" \ + --password "$CI_REGISTRY_PASSWORD" \ + --authfile "$REGISTRY_AUTH_FILE" \ + "$CI_REGISTRY_IMAGE"; + image=(./builds/*-docker-*); + ./scripts/deploy-image.sh "${image[0]}" \'testnet\' "$CI_REGISTRY_IMAGE"; + ' + after_script: + - rm -f "$REGISTRY_AUTH_FILE" + rules: + # Only runs on tag pipeline where the tag is a prerelease version + # This requires dependencies to also run on tag pipeline + # However version tag comes with a version commit + # Dependencies must not run on the version commit + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+-.*[0-9]+$/ + +integration:merge: + stage: integration + needs: + - build:merge + - job: build:platforms + optional: true + - job: integration:nix + optional: true + - job: integration:docker + optional: true + - job: integration:linux + optional: true + # - job: integration:windows + # optional: true + # - job: integration:macos + # optional: true + # Requires mutual exclusion + resource_group: integration:merge + allow_failure: true + variables: + # Ensure that CI/CD is fetching all commits + # this is necessary to checkout origin/master + # and to also merge origin/staging + GIT_DEPTH: 0 + script: + - > + nix-shell --arg ci true --run $' + printf "Pipeline Succeeded on ${CI_PIPELINE_ID} for ${CI_COMMIT_SHA}\n\n${CI_PIPELINE_URL}" \ + | gh pr comment staging \ + --body-file - \ + --repo "$GH_PROJECT_PATH"; + ' + - git remote add upstream "$GH_PROJECT_URL" + - git checkout origin/master + # Merge up to the current commit (not the latest commit) + - git merge --ff-only "$CI_COMMIT_SHA" + - git push upstream HEAD:master + rules: + # Runs on staging commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + # Runs on tag pipeline where the tag is a prerelease or release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +release:deployment:branch: + stage: release + # Only needs integration:builds from the staging branch pipeline + needs: + - project: $CI_PROJECT_PATH + job: integration:builds + ref: staging + artifacts: true + # Don't interrupt deploying job + interruptible: false + # Requires mutual exclusion (also with release:deployment:tag) + resource_group: release:deployment + environment: + name: 'mainnet' + deployment_tier: 'production' + url: 'https://mainnet.polykey.io' + variables: + REGISTRY_AUTH_FILE: "./tmp/registry-auth-file.json" + # Override CI_REGISTRY_IMAGE to point to ECR + CI_REGISTRY_IMAGE: '015248367786.dkr.ecr.ap-southeast-2.amazonaws.com/polykey' + script: + - echo 'Deploying container image to ECR' + - > + nix-shell --arg ci true --run $' + aws ecr get-login-password \ + | skopeo login \ + --username AWS \ + --password-stdin \ + --authfile "$REGISTRY_AUTH_FILE" \ + "$CI_REGISTRY_IMAGE"; + image=(./builds/*-docker-*); + ./scripts/deploy-image.sh "${image[0]}" \'mainnet\' "$CI_REGISTRY_IMAGE"; + echo \'Deploying ECS service to mainnet\'; + ' + after_script: + - rm -f "$REGISTRY_AUTH_FILE" + rules: + # Runs on master commits and ignores version commits + - if: $CI_COMMIT_BRANCH == 'master' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ + +release:deployment:tag: + stage: release + # Tag pipelines run independently + needs: + - integration:builds + - integration:merge + # Don't interrupt deploying job + interruptible: false + # Requires mutual exclusion (also with release:deployment:branch) + resource_group: release:deployment + environment: + name: 'mainnet' + deployment_tier: 'production' + url: 'https://mainnet.polykey.io' + variables: + REGISTRY_AUTH_FILE: "./tmp/registry-auth-file.json" + # Override CI_REGISTRY_IMAGE to point to ECR + CI_REGISTRY_IMAGE: '015248367786.dkr.ecr.ap-southeast-2.amazonaws.com/polykey' + script: + - echo 'Deploying container image to ECR' + - > + nix-shell --arg ci true --run $' + aws ecr get-login-password \ + | skopeo login \ + --username AWS \ + --password-stdin \ + --authfile "$REGISTRY_AUTH_FILE" \ + "$CI_REGISTRY_IMAGE"; + image=(./builds/*-docker-*); + ./scripts/deploy-image.sh "${image[0]}" \'mainnet\' "$CI_REGISTRY_IMAGE"; + echo \'Deploying ECS service to mainnet\'; + ' + after_script: + - rm -f "$REGISTRY_AUTH_FILE" + rules: + # Runs on tag pipeline where the tag is a release version + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ + +release:distribution: + stage: release + needs: + - build:dist + - build:platforms + - integration:builds + - integration:merge + - release:deployment:tag + # Don't interrupt publishing job + interruptible: false + # Requires mutual exclusion + resource_group: release:distribution + variables: + REGISTRY_AUTH_FILE: "./tmp/registry-auth-file.json" + script: + - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ./.npmrc + - echo 'Publishing library' + - > + nix-shell --arg ci true --run $' + npm publish --access public; + ' + - echo 'Releasing application builds' + - > + nix-shell --arg ci true --run $' + gh release \ + create "$CI_COMMIT_TAG" \ + builds/*.closure.gz \ + builds/*-docker-* \ + builds/*-linux-* \ + builds/*-win-* \ + builds/*-macos-* \ + --title "${CI_COMMIT_TAG}-$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ + --notes "" \ + --target master \ + --repo "$GH_PROJECT_PATH"; + ' + - echo 'Releasing container image' + - > + nix-shell --arg ci true --run $' + skopeo login \ + --username "$CI_REGISTRY_USER" \ + --password "$CI_REGISTRY_PASSWORD" \ + --authfile "$REGISTRY_AUTH_FILE" \ + "$CI_REGISTRY_IMAGE"; + image=(./builds/*-docker-*); + ./scripts/deploy-image.sh "${image[0]}" \'mainnet\' "$CI_REGISTRY_IMAGE"; + ' + after_script: + - rm -f ./.npmrc + - rm -f "$REGISTRY_AUTH_FILE" + rules: + # Only runs on tag pipeline where the tag is a release version + # This requires dependencies to also run on tag pipeline + # However version tag comes with a version commit + # Dependencies must not run on the version commit + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..ea6884b3 --- /dev/null +++ b/.npmignore @@ -0,0 +1,17 @@ +.* +/*.nix +/nix +/tsconfig.json +/tsconfig.build.json +/babel.config.js +/jest.config.js +/nodemon.json +/scripts +/src +/tests +/tmp +/docs +/benches +/build +/builds +/dist/tsbuildinfo diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..7c06da2c --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +# Enables npm link +prefix=~/.npm diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..fa9699b8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2 +} diff --git a/README.md b/README.md index e55642cc..5aa341c5 100644 --- a/README.md +++ b/README.md @@ -1 +1,289 @@ # Polykey-CLI + +staging:[![pipeline status](https://gitlab.com/MatrixAI/open-source/Polykey-CLI/badges/staging/pipeline.svg)](https://gitlab.com/MatrixAI/open-source/Polykey-CLI/commits/staging) +master:[![pipeline status](https://gitlab.com/MatrixAI/open-source/Polykey-CLI/badges/master/pipeline.svg)](https://gitlab.com/MatrixAI/open-source/Polykey-CLI/commits/master) + +Polykey is an open-source decentralized secrets management and sharing system. It is made for today's decentralized world of people, services and devices. + +* Decentralized Encrypted Storage - No storage of secrets on third parties, secrets are stored on your device and synchronised point-to-point between Polykey nodes. +* Secure Peer-to-Peer Communications - Polykey bootstraps TLS keys by federating trusted social identities (e.g. GitHub). +* Secure Computational Workflows - Share secrets (passwords, keys, tokens and certificates) with people, between teams, and across machine infrastructure. + +

+ Polykey CLI Demo +

+ +Polykey synthesizes a unified workflow between interactive password management and infrastructure key management. + +You have complete end-to-end control and privacy over your secrets, with no third-party data collection. + +Polykey runs on distributed keynodes referred to as "nodes". Any computing system can run multiple keynodes. Each node manages one or more vaults which are encrypted filesystems with automatic version history. Vaults can be shared between the nodes. + +This repository is the core library for Polykey. + +The Polykey project is split up into these main repositories: + +* [Polykey](https://github.com/MatrixAI/Polykey) - Polykey Core Library +* [Polykey-CLI](https://github.com/MatrixAI/Polykey-CLI) - CLI of Polykey +* [Polykey-Desktop](https://github.com/MatrixAI/Polykey-Desktop) - Polykey Desktop (Windows, Mac, Linux) application +* [Polykey-Mobile](https://github.com/MatrixAI/Polykey-Mobile) - Polykey Mobile (iOS & Android) Application + +Have a bug or a feature-request? Please submit it the issues of the relevant subproject above. + +For tutorials, how-to guides, reference and theory, see the [docs](https://polykey.io/docs). + +Have a question? Join our [discussion board](https://github.com/MatrixAI/Polykey/discussions). + +Our main website is https://polykey.io + +## Installation + +Note that JavaScript libraries are not packaged in Nix. Only JavaScript applications are. + +Building the package: + +```sh +nix-build -E '(import ./pkgs.nix {}).callPackage ./default.nix {}' +``` + +### Nix/NixOS + +Building the releases: + +```sh +nix-build ./release.nix --attr application +nix-build ./release.nix --attr docker +nix-build ./release.nix --attr package.linux.x64.elf +nix-build ./release.nix --attr package.windows.x64.exe +nix-build ./release.nix --attr package.macos.x64.macho +``` + +Install into Nix user profile: + +```sh +nix-env -f ./release.nix --install --attr application +``` + +### Docker + +Install into Docker: + +```sh +loaded="$(docker load --input "$(nix-build ./release.nix --attr docker)")" +image="$(cut -d' ' -f3 <<< "$loaded")" +docker run -it "$image" +``` + +## Development + +Run `nix-shell`, and once you're inside, you can use: + +```sh +# install (or reinstall packages from package.json) +npm install +# build the dist +npm run build +# run the repl (this allows you to import from ./src) +npm run ts-node +# run the tests +npm run test +# lint the source code +npm run lint +# automatically fix the source +npm run lintfix +``` + +### Calling Commands + +When calling commands in development, use this style: + +```sh +npm run polykey -- p1 p2 p3 +``` + +The `--` is necessary to make `npm` understand that the parameters are for your own executable, and not parameters to `npm`. + +### Using the REPL + +``` +$ npm run ts-node +> import fs from 'fs'; +> fs +> import { Library } from '@'; +> Library +> import Library as Library2 from './src/lib/Library'; +``` + +You can also create test files in `./src`, and run them with `npm run ts-node ./src/test.ts`. + +This allows you to test individual pieces of typescript code, and it makes it easier when doing large scale architecting of TypeScript code. + +### Path Aliases + +Due to https://github.com/microsoft/TypeScript/issues/10866, you cannot use path aliases without a bundler like Webpack to further transform the generated JavaScript code in order to resolve the path aliases. Because this is a simple library demonstration, there's no need to use a bundler. In fact, for such libraries, it is far more efficient to not bundle the code. + +However, we have left the path alias configuration in `tsconfig.json`, `jest.config.js` and in the tests we are making use of the `@` alias. + +### Local Package Linking + +When developing on multiple NPM packages, it can be easier to use `npm link` so that changes are immediately reflected rather than repeatedly publishing packages. To do this, you need to use `npm link`. After linking a local directory, you need to provide `tsconfig.json` paths so TypeScript compiler can find the right files. + +For example when linking `@matrixai/db` located in `../js-db`: + +```sh +npm link ../js-db +``` + +You would need to add these paths to `tsconfig.json`: + +``` + "paths": { + "@": ["index"], + "@/*": ["*"], + "@matrixai/db": ["../node_modules/@matrixai/db/src"], + "@matrixai/db/*": ["../node_modules/@matrixai/db/src/*"] + }, +``` + +### Native Module Toolchain + +There are some nuances when packaging with native modules. +Included native modules are level witch include leveldown and utp-native. + +If a module is not set to public then pkg defaults to including it as bytecode. +To avoid this breaking with the `--no-bytecode` flag we need to add `--public-packages "*"` + +#### leveldown + +To get leveldown to work with pkg we need to include the prebuilds with the executable. +after building with pkg you need to copy from `node_modules/leveldown/prebuilds` -> `path_to_executable/prebuilds` +You only need to include the prebuilds for the arch you are targeting. e.g. for linux-x64 you need `prebuild/linux-x64`. + +The folder structure for the executable should look like this. +- linux_executable_elf +- prebuilds + - linux-x64 + - (node files) + +#### threads.js + +To make sure that the worker threads work properly you need to include the compiled worker scripts as an asset. +This can be fixed by adding the following to `package.json` + +```json +"pkg": { + "assets": "dist/bin/worker.js" + } +``` + +If you need to include multiple assets then add them as an array. + +```json +"pkg": { + "assets": [ + "node_modules/utp-native/**/*", + "dist/bin/worker.js" + ] + } +``` + +### Docs Generation + +```sh +npm run docs +``` + +See the docs at: https://matrixai.github.io/TypeScript-Demo-Lib/ + +### Publishing + +Publishing is handled automatically by the staging pipeline. + +Prerelease: + +```sh +# npm login +npm version prepatch --preid alpha # premajor/preminor/prepatch +git push --follow-tags +``` + +Release: + +```sh +# npm login +npm version patch # major/minor/patch +git push --follow-tags +``` + +Manually: + +```sh +# npm login +npm version patch # major/minor/patch +npm run build +npm publish --access public +git push +git push --tags +``` +### Packaging Cross-Platform Executables + +We use `pkg` to package the source code into executables. + +This requires a specific version of `pkg` and also `node-gyp-build`. + +Configuration for `pkg` is done in: + +* `package.json` - Pins `pkg` and `node-gyp-build`, and configures assets and scripts. +* `utils.nix` - Pins `pkg` for Nix usage +* `release.nix` - Build expressions for executables + +## Deployment + +Image deployments are done automatically through the CI/CD. However manual scripts are available below for deployment. + +### Deploying to AWS ECR: + +#### Using skopeo + +```sh +tag='manual' +registry_image='015248367786.dkr.ecr.ap-southeast-2.amazonaws.com/polykey' + +# Authenticates skopeo +aws ecr get-login-password \ + | skopeo login \ + --username AWS \ + --password-stdin \ + "$registry_image" + +build="$(nix-build ./release.nix --attr docker)" +# This will push both the default image tag and the latest tag +./scripts/deploy-image.sh "$build" "$tag" "$registry_image" +``` + +#### Using docker + +```sh +tag='manual' +registry_image='015248367786.dkr.ecr.ap-southeast-2.amazonaws.com/polykey' + +aws ecr get-login-password \ + | docker login \ + --username AWS \ + --password-stdin \ + "$registry_image" + +build="$(nix-build ./release.nix --attr docker)" +loaded="$(docker load --input "$build")" +image_name="$(cut -d':' -f2 <<< "$loaded" | tr -d ' ')" +default_tag="$(cut -d':' -f3 <<< "$loaded")" + +docker tag "${image_name}:${default_tag}" "${registry_image}:${default_tag}" +docker tag "${image_name}:${default_tag}" "${registry_image}:${tag}" +docker tag "${image_name}:${default_tag}" "${registry_image}:latest" + +docker push "${registry_image}:${default_tag}" +docker push "${registry_image}:${tag}" +docker push "${registry_image}:latest" +``` + diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..283de766 --- /dev/null +++ b/default.nix @@ -0,0 +1,57 @@ +{ runCommandNoCC +, callPackage +, jq +}: + +let + utils = callPackage ./utils.nix {}; + drv = runCommandNoCC + "${utils.basename}-${utils.node2nixDev.version}" + { + version = utils.node2nixDev.version; + packageName = utils.node2nixDev.packageName; + } + '' + mkdir -p "$out/lib/node_modules/$packageName" + # copy the package.json + cp \ + "${utils.node2nixDev}/lib/node_modules/$packageName/package.json" \ + "$out/lib/node_modules/$packageName/" + # copy the native addons + if [ -d "${utils.node2nixDev}/lib/node_modules/$packageName/prebuilds" ]; then + cp -r \ + "${utils.node2nixDev}/lib/node_modules/$packageName/prebuilds" \ + "$out/lib/node_modules/$packageName/" + fi + # copy the dist + cp -r \ + "${utils.node2nixDev}/lib/node_modules/$packageName/dist" \ + "$out/lib/node_modules/$packageName/" + # copy over the production dependencies + if [ -d "${utils.node2nixProd}/lib/node_modules" ]; then + cp -r \ + "${utils.node2nixProd}/lib/node_modules" \ + "$out/lib/node_modules/$packageName/" + fi + # symlink bin executables + if [ \ + "$(${jq}/bin/jq 'has("bin")' "$out/lib/node_modules/$packageName/package.json")" \ + == \ + "true" \ + ]; then + mkdir -p "$out/bin" + while IFS= read -r bin_name && IFS= read -r bin_path; do + # make files executable + chmod a+x "$out/lib/node_modules/$packageName/$bin_path" + # create the symlink + ln -s \ + "../lib/node_modules/$packageName/$bin_path" \ + "$out/bin/$bin_name" + done < <( + ${jq}/bin/jq -r 'select(.bin != null) | .bin | to_entries[] | (.key, .value)' \ + "$out/lib/node_modules/$packageName/package.json" + ) + fi + ''; +in + drv diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e2ac6616 --- /dev/null +++ b/docs/.nojekyll @@ -0,0 +1 @@ +TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/docs/assets/highlight.css b/docs/assets/highlight.css new file mode 100644 index 00000000..1c8c07d5 --- /dev/null +++ b/docs/assets/highlight.css @@ -0,0 +1,78 @@ +:root { + --light-hl-0: #795E26; + --dark-hl-0: #DCDCAA; + --light-hl-1: #000000; + --dark-hl-1: #D4D4D4; + --light-hl-2: #0000FF; + --dark-hl-2: #569CD6; + --light-hl-3: #A31515; + --dark-hl-3: #CE9178; + --light-hl-4: #001080; + --dark-hl-4: #9CDCFE; + --light-hl-5: #008000; + --dark-hl-5: #6A9955; + --light-hl-6: #AF00DB; + --dark-hl-6: #C586C0; + --light-hl-7: #0451A5; + --dark-hl-7: #9CDCFE; + --light-code-background: #FFFFFF; + --dark-code-background: #1E1E1E; +} + +@media (prefers-color-scheme: light) { :root { + --hl-0: var(--light-hl-0); + --hl-1: var(--light-hl-1); + --hl-2: var(--light-hl-2); + --hl-3: var(--light-hl-3); + --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --code-background: var(--light-code-background); +} } + +@media (prefers-color-scheme: dark) { :root { + --hl-0: var(--dark-hl-0); + --hl-1: var(--dark-hl-1); + --hl-2: var(--dark-hl-2); + --hl-3: var(--dark-hl-3); + --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --code-background: var(--dark-code-background); +} } + +:root[data-theme='light'] { + --hl-0: var(--light-hl-0); + --hl-1: var(--light-hl-1); + --hl-2: var(--light-hl-2); + --hl-3: var(--light-hl-3); + --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --code-background: var(--light-code-background); +} + +:root[data-theme='dark'] { + --hl-0: var(--dark-hl-0); + --hl-1: var(--dark-hl-1); + --hl-2: var(--dark-hl-2); + --hl-3: var(--dark-hl-3); + --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --code-background: var(--dark-code-background); +} + +.hl-0 { color: var(--hl-0); } +.hl-1 { color: var(--hl-1); } +.hl-2 { color: var(--hl-2); } +.hl-3 { color: var(--hl-3); } +.hl-4 { color: var(--hl-4); } +.hl-5 { color: var(--hl-5); } +.hl-6 { color: var(--hl-6); } +.hl-7 { color: var(--hl-7); } +pre, code { background: var(--code-background); } diff --git a/docs/assets/main.js b/docs/assets/main.js new file mode 100644 index 00000000..f7c83669 --- /dev/null +++ b/docs/assets/main.js @@ -0,0 +1,58 @@ +"use strict"; +"use strict";(()=>{var Qe=Object.create;var ae=Object.defineProperty;var Pe=Object.getOwnPropertyDescriptor;var Ce=Object.getOwnPropertyNames;var Oe=Object.getPrototypeOf,Re=Object.prototype.hasOwnProperty;var _e=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var Me=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Ce(e))!Re.call(t,i)&&i!==n&&ae(t,i,{get:()=>e[i],enumerable:!(r=Pe(e,i))||r.enumerable});return t};var De=(t,e,n)=>(n=t!=null?Qe(Oe(t)):{},Me(e||!t||!t.__esModule?ae(n,"default",{value:t,enumerable:!0}):n,t));var de=_e((ce,he)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(n){e.console&&console.warn&&console.warn(n)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var h=t.utils.clone(n)||{};h.position=[a,l],h.index=s.length,s.push(new t.Token(r.slice(a,o),h))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. +`,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ou?h+=2:a==u&&(n+=r[l+1]*i[h+1],l+=2,h+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}if(s.str.length==0&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}s.str.length==1&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var h=s.str.charAt(0),m=s.str.charAt(1),v;m in s.node.edges?v=s.node.edges[m]:(v=new t.TokenSet,s.node.edges[m]=v),s.str.length==1&&(v.final=!0),i.push({node:v,editsRemaining:s.editsRemaining-1,str:h+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,n){typeof define=="function"&&define.amd?define(n):typeof ce=="object"?he.exports=n():e.lunr=n()}(this,function(){return t})})()});var le=[];function B(t,e){le.push({selector:e,constructor:t})}var Y=class{constructor(){this.alwaysVisibleMember=null;this.createComponents(document.body),this.ensureFocusedElementVisible(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible())}createComponents(e){le.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}ensureFocusedElementVisible(){this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null);let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(n&&n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let r=document.createElement("p");r.classList.add("warning"),r.textContent="This member is normally hidden due to your filter settings.",n.prepend(r)}}};var I=class{constructor(e){this.el=e.el,this.app=e.app}};var J=class{constructor(){this.listeners={}}addEventListener(e,n){e in this.listeners||(this.listeners[e]=[]),this.listeners[e].push(n)}removeEventListener(e,n){if(!(e in this.listeners))return;let r=this.listeners[e];for(let i=0,s=r.length;i{let n=Date.now();return(...r)=>{n+e-Date.now()<0&&(t(...r),n=Date.now())}};var re=class extends J{constructor(){super();this.scrollTop=0;this.lastY=0;this.width=0;this.height=0;this.showToolbar=!0;this.toolbar=document.querySelector(".tsd-page-toolbar"),this.navigation=document.querySelector(".col-menu"),window.addEventListener("scroll",ne(()=>this.onScroll(),10)),window.addEventListener("resize",ne(()=>this.onResize(),10)),this.searchInput=document.querySelector("#tsd-search input"),this.searchInput&&this.searchInput.addEventListener("focus",()=>{this.hideShowToolbar()}),this.onResize(),this.onScroll()}triggerResize(){let n=new CustomEvent("resize",{detail:{width:this.width,height:this.height}});this.dispatchEvent(n)}onResize(){this.width=window.innerWidth||0,this.height=window.innerHeight||0;let n=new CustomEvent("resize",{detail:{width:this.width,height:this.height}});this.dispatchEvent(n)}onScroll(){this.scrollTop=window.scrollY||0;let n=new CustomEvent("scroll",{detail:{scrollTop:this.scrollTop}});this.dispatchEvent(n),this.hideShowToolbar()}hideShowToolbar(){let n=this.showToolbar;this.showToolbar=this.lastY>=this.scrollTop||this.scrollTop<=0||!!this.searchInput&&this.searchInput===document.activeElement,n!==this.showToolbar&&(this.toolbar.classList.toggle("tsd-page-toolbar--hide"),this.navigation?.classList.toggle("col-menu--hide")),this.lastY=this.scrollTop}},R=re;R.instance=new re;var X=class extends I{constructor(n){super(n);this.anchors=[];this.index=-1;R.instance.addEventListener("resize",()=>this.onResize()),R.instance.addEventListener("scroll",r=>this.onScroll(r)),this.createAnchors()}createAnchors(){let n=window.location.href;n.indexOf("#")!=-1&&(n=n.substring(0,n.indexOf("#"))),this.el.querySelectorAll("a").forEach(r=>{let i=r.href;if(i.indexOf("#")==-1||i.substring(0,n.length)!=n)return;let s=i.substring(i.indexOf("#")+1),o=document.querySelector("a.tsd-anchor[name="+s+"]"),a=r.parentNode;!o||!a||this.anchors.push({link:a,anchor:o,position:0})}),this.onResize()}onResize(){let n;for(let i=0,s=this.anchors.length;ii.position-s.position);let r=new CustomEvent("scroll",{detail:{scrollTop:R.instance.scrollTop}});this.onScroll(r)}onScroll(n){let r=n.detail.scrollTop+5,i=this.anchors,s=i.length-1,o=this.index;for(;o>-1&&i[o].position>r;)o-=1;for(;o-1&&this.anchors[this.index].link.classList.remove("focus"),this.index=o,this.index>-1&&this.anchors[this.index].link.classList.add("focus"))}};var ue=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var me=De(de());function ve(){let t=document.getElementById("tsd-search");if(!t)return;let e=document.getElementById("search-script");t.classList.add("loading"),e&&(e.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),e.addEventListener("load",()=>{t.classList.remove("loading"),t.classList.add("ready")}),window.searchData&&t.classList.remove("loading"));let n=document.querySelector("#tsd-search input"),r=document.querySelector("#tsd-search .results");if(!n||!r)throw new Error("The input field or the result list wrapper was not found");let i=!1;r.addEventListener("mousedown",()=>i=!0),r.addEventListener("mouseup",()=>{i=!1,t.classList.remove("has-focus")}),n.addEventListener("focus",()=>t.classList.add("has-focus")),n.addEventListener("blur",()=>{i||(i=!1,t.classList.remove("has-focus"))});let s={base:t.dataset.base+"/"};Fe(t,r,n,s)}function Fe(t,e,n,r){n.addEventListener("input",ue(()=>{He(t,e,n,r)},200));let i=!1;n.addEventListener("keydown",s=>{i=!0,s.key=="Enter"?Ve(e,n):s.key=="Escape"?n.blur():s.key=="ArrowUp"?pe(e,-1):s.key==="ArrowDown"?pe(e,1):i=!1}),n.addEventListener("keypress",s=>{i&&s.preventDefault()}),document.body.addEventListener("keydown",s=>{s.altKey||s.ctrlKey||s.metaKey||!n.matches(":focus")&&s.key==="/"&&(n.focus(),s.preventDefault())})}function Ae(t,e){t.index||window.searchData&&(e.classList.remove("loading"),e.classList.add("ready"),t.data=window.searchData,t.index=me.Index.load(window.searchData.index))}function He(t,e,n,r){if(Ae(r,t),!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s=i?r.index.search(`*${i}*`):[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o${fe(u.parent,i)}.${l}`);let h=document.createElement("li");h.classList.value=u.classes??"";let m=document.createElement("a");m.href=r.base+u.url,m.innerHTML=l,h.append(m),e.appendChild(h)}}function pe(t,e){let n=t.querySelector(".current");if(!n)n=t.querySelector(e==1?"li:first-child":"li:last-child"),n&&n.classList.add("current");else{let r=n;if(e===1)do r=r.nextElementSibling??void 0;while(r instanceof HTMLElement&&r.offsetParent==null);else do r=r.previousElementSibling??void 0;while(r instanceof HTMLElement&&r.offsetParent==null);r&&(n.classList.remove("current"),r.classList.add("current"))}}function Ve(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),e.blur()}}function fe(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(ie(t.substring(s,o)),`${ie(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(ie(t.substring(s))),i.join("")}var Ne={"&":"&","<":"<",">":">","'":"'",'"':"""};function ie(t){return t.replace(/[&<>"'"]/g,e=>Ne[e])}var F="mousedown",ye="mousemove",j="mouseup",Z={x:0,y:0},ge=!1,se=!1,Be=!1,A=!1,xe=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(xe?"is-mobile":"not-mobile");xe&&"ontouchstart"in document.documentElement&&(Be=!0,F="touchstart",ye="touchmove",j="touchend");document.addEventListener(F,t=>{se=!0,A=!1;let e=F=="touchstart"?t.targetTouches[0]:t;Z.y=e.pageY||0,Z.x=e.pageX||0});document.addEventListener(ye,t=>{if(se&&!A){let e=F=="touchstart"?t.targetTouches[0]:t,n=Z.x-(e.pageX||0),r=Z.y-(e.pageY||0);A=Math.sqrt(n*n+r*r)>10}});document.addEventListener(j,()=>{se=!1});document.addEventListener("click",t=>{ge&&(t.preventDefault(),t.stopImmediatePropagation(),ge=!1)});var K=class extends I{constructor(n){super(n);this.className=this.el.dataset.toggle||"",this.el.addEventListener(j,r=>this.onPointerUp(r)),this.el.addEventListener("click",r=>r.preventDefault()),document.addEventListener(F,r=>this.onDocumentPointerDown(r)),document.addEventListener(j,r=>this.onDocumentPointerUp(r))}setActive(n){if(this.active==n)return;this.active=n,document.documentElement.classList.toggle("has-"+this.className,n),this.el.classList.toggle("active",n);let r=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(r),setTimeout(()=>document.documentElement.classList.remove(r),500)}onPointerUp(n){A||(this.setActive(!0),n.preventDefault())}onDocumentPointerDown(n){if(this.active){if(n.target.closest(".col-menu, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(n){if(!A&&this.active&&n.target.closest(".col-menu")){let r=n.target.closest("a");if(r){let i=window.location.href;i.indexOf("#")!=-1&&(i=i.substring(0,i.indexOf("#"))),r.href.substring(0,i.length)==i&&setTimeout(()=>this.setActive(!1),250)}}}};var oe;try{oe=localStorage}catch{oe={getItem(){return null},setItem(){}}}var Q=oe;var Le=document.head.appendChild(document.createElement("style"));Le.dataset.for="filters";var ee=class extends I{constructor(n){super(n);this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),Le.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } +`}fromLocalStorage(){let n=Q.getItem(this.key);return n?n==="true":this.el.checked}setLocalStorage(n){Q.setItem(this.key,n.toString()),this.value=n,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),this.app.filterChanged(),document.querySelectorAll(".tsd-index-section").forEach(n=>{n.style.display="block";let r=Array.from(n.querySelectorAll(".tsd-index-link")).every(i=>i.offsetParent==null);n.style.display=r?"none":"block"})}};var te=class extends I{constructor(n){super(n);this.calculateHeights(),this.summary=this.el.querySelector(".tsd-accordion-summary"),this.icon=this.summary.querySelector("svg"),this.key=`tsd-accordion-${this.summary.textContent.replace(/\s+/g,"-").toLowerCase()}`,this.setLocalStorage(this.fromLocalStorage(),!0),this.summary.addEventListener("click",r=>this.toggleVisibility(r)),this.icon.style.transform=this.getIconRotation()}getIconRotation(n=this.el.open){return`rotate(${n?0:-90}deg)`}calculateHeights(){let n=this.el.open,{position:r,left:i}=this.el.style;this.el.style.position="fixed",this.el.style.left="-9999px",this.el.open=!0,this.expandedHeight=this.el.offsetHeight+"px",this.el.open=!1,this.collapsedHeight=this.el.offsetHeight+"px",this.el.open=n,this.el.style.height=n?this.expandedHeight:this.collapsedHeight,this.el.style.position=r,this.el.style.left=i}toggleVisibility(n){n.preventDefault(),this.el.style.overflow="hidden",this.el.open?this.collapse():this.expand()}expand(n=!0){this.el.open=!0,this.animate(this.collapsedHeight,this.expandedHeight,{opening:!0,duration:n?300:0})}collapse(n=!0){this.animate(this.expandedHeight,this.collapsedHeight,{opening:!1,duration:n?300:0})}animate(n,r,{opening:i,duration:s=300}){if(this.animation)return;let o={duration:s,easing:"ease"};this.animation=this.el.animate({height:[n,r]},o),this.icon.animate({transform:[this.icon.style.transform||this.getIconRotation(!i),this.getIconRotation(i)]},o).addEventListener("finish",()=>{this.icon.style.transform=this.getIconRotation(i)}),this.animation.addEventListener("finish",()=>this.animationEnd(i))}animationEnd(n){this.el.open=n,this.animation=void 0,this.el.style.height="auto",this.el.style.overflow="visible",this.setLocalStorage(n)}fromLocalStorage(){let n=Q.getItem(this.key);return n?n==="true":this.el.open}setLocalStorage(n,r=!1){this.fromLocalStorage()===n&&!r||(Q.setItem(this.key,n.toString()),this.el.open=n,this.handleValueChange(r))}handleValueChange(n=!1){this.fromLocalStorage()===this.el.open&&!n||(this.fromLocalStorage()?this.expand(!1):this.collapse(!1))}};function be(t){let e=Q.getItem("tsd-theme")||"os";t.value=e,Ee(e),t.addEventListener("change",()=>{Q.setItem("tsd-theme",t.value),Ee(t.value)})}function Ee(t){document.documentElement.dataset.theme=t}ve();B(X,".menu-highlight");B(K,"a[data-toggle]");B(te,".tsd-index-accordion");B(ee,".tsd-filter-item input[type=checkbox]");var we=document.getElementById("theme");we&&be(we);var je=new Y;Object.defineProperty(window,"app",{value:je});})(); +/*! Bundled license information: + +lunr/lunr.js: + (** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + *) + (*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + *) + (*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + *) +*/ diff --git a/docs/assets/search.js b/docs/assets/search.js new file mode 100644 index 00000000..d5a0b186 --- /dev/null +++ b/docs/assets/search.js @@ -0,0 +1 @@ +window.searchData = JSON.parse("{\"kinds\":{\"128\":\"Class\",\"512\":\"Constructor\",\"1024\":\"Property\"},\"rows\":[{\"kind\":128,\"name\":\"Library\",\"url\":\"classes/Library.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/Library.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"Library\"},{\"kind\":1024,\"name\":\"someParam\",\"url\":\"classes/Library.html#someParam\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"Library\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,9.808]],[\"comment/0\",[]],[\"name/1\",[1,9.808]],[\"comment/1\",[]],[\"name/2\",[2,9.808]],[\"comment/2\",[]]],\"invertedIndex\":[[\"constructor\",{\"_index\":1,\"name\":{\"1\":{}},\"comment\":{}}],[\"library\",{\"_index\":0,\"name\":{\"0\":{}},\"comment\":{}}],[\"someparam\",{\"_index\":2,\"name\":{\"2\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/docs/assets/style.css b/docs/assets/style.css new file mode 100644 index 00000000..496e66f2 --- /dev/null +++ b/docs/assets/style.css @@ -0,0 +1,1279 @@ +:root { + /* Light */ + --light-color-background: #f2f4f8; + --light-color-background-secondary: #eff0f1; + --light-color-warning-text: #222; + --light-color-background-warning: #e6e600; + --light-color-icon-background: var(--light-color-background); + --light-color-accent: #c5c7c9; + --light-color-text: #222; + --light-color-text-aside: #707070; + --light-color-link: #4da6ff; + --light-color-ts: #db1373; + --light-color-ts-interface: #139d2c; + --light-color-ts-enum: #9c891a; + --light-color-ts-class: #2484e5; + --light-color-ts-function: #572be7; + --light-color-ts-namespace: #b111c9; + --light-color-ts-private: #707070; + --light-color-ts-variable: #4d68ff; + --light-external-icon: url("data:image/svg+xml;utf8,"); + --light-color-scheme: light; + + /* Dark */ + --dark-color-background: #2b2e33; + --dark-color-background-secondary: #1e2024; + --dark-color-background-warning: #bebe00; + --dark-color-warning-text: #222; + --dark-color-icon-background: var(--dark-color-background-secondary); + --dark-color-accent: #9096a2; + --dark-color-text: #f5f5f5; + --dark-color-text-aside: #dddddd; + --dark-color-link: #00aff4; + --dark-color-ts: #ff6492; + --dark-color-ts-interface: #6cff87; + --dark-color-ts-enum: #f4d93e; + --dark-color-ts-class: #61b0ff; + --dark-color-ts-function: #9772ff; + --dark-color-ts-namespace: #e14dff; + --dark-color-ts-private: #e2e2e2; + --dark-color-ts-variable: #4d68ff; + --dark-external-icon: url("data:image/svg+xml;utf8,"); + --dark-color-scheme: dark; +} + +@media (prefers-color-scheme: light) { + :root { + --color-background: var(--light-color-background); + --color-background-secondary: var(--light-color-background-secondary); + --color-background-warning: var(--light-color-background-warning); + --color-warning-text: var(--light-color-warning-text); + --color-icon-background: var(--light-color-icon-background); + --color-accent: var(--light-color-accent); + --color-text: var(--light-color-text); + --color-text-aside: var(--light-color-text-aside); + --color-link: var(--light-color-link); + --color-ts: var(--light-color-ts); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-class: var(--light-color-ts-class); + --color-ts-function: var(--light-color-ts-function); + --color-ts-namespace: var(--light-color-ts-namespace); + --color-ts-private: var(--light-color-ts-private); + --color-ts-variable: var(--light-color-ts-variable); + --external-icon: var(--light-external-icon); + --color-scheme: var(--light-color-scheme); + } +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--dark-color-background); + --color-background-secondary: var(--dark-color-background-secondary); + --color-background-warning: var(--dark-color-background-warning); + --color-warning-text: var(--dark-color-warning-text); + --color-icon-background: var(--dark-color-icon-background); + --color-accent: var(--dark-color-accent); + --color-text: var(--dark-color-text); + --color-text-aside: var(--dark-color-text-aside); + --color-link: var(--dark-color-link); + --color-ts: var(--dark-color-ts); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-function: var(--dark-color-ts-function); + --color-ts-namespace: var(--dark-color-ts-namespace); + --color-ts-private: var(--dark-color-ts-private); + --color-ts-variable: var(--dark-color-ts-variable); + --external-icon: var(--dark-external-icon); + --color-scheme: var(--dark-color-scheme); + } +} + +html { + color-scheme: var(--color-scheme); +} + +body { + margin: 0; +} + +:root[data-theme="light"] { + --color-background: var(--light-color-background); + --color-background-secondary: var(--light-color-background-secondary); + --color-background-warning: var(--light-color-background-warning); + --color-warning-text: var(--light-color-warning-text); + --color-icon-background: var(--light-color-icon-background); + --color-accent: var(--light-color-accent); + --color-text: var(--light-color-text); + --color-text-aside: var(--light-color-text-aside); + --color-link: var(--light-color-link); + --color-ts: var(--light-color-ts); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-class: var(--light-color-ts-class); + --color-ts-function: var(--light-color-ts-function); + --color-ts-namespace: var(--light-color-ts-namespace); + --color-ts-private: var(--light-color-ts-private); + --color-ts-variable: var(--light-color-ts-variable); + --external-icon: var(--light-external-icon); + --color-scheme: var(--light-color-scheme); +} + +:root[data-theme="dark"] { + --color-background: var(--dark-color-background); + --color-background-secondary: var(--dark-color-background-secondary); + --color-background-warning: var(--dark-color-background-warning); + --color-warning-text: var(--dark-color-warning-text); + --color-icon-background: var(--dark-color-icon-background); + --color-accent: var(--dark-color-accent); + --color-text: var(--dark-color-text); + --color-text-aside: var(--dark-color-text-aside); + --color-link: var(--dark-color-link); + --color-ts: var(--dark-color-ts); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-function: var(--dark-color-ts-function); + --color-ts-namespace: var(--dark-color-ts-namespace); + --color-ts-private: var(--dark-color-ts-private); + --color-ts-variable: var(--dark-color-ts-variable); + --external-icon: var(--dark-external-icon); + --color-scheme: var(--dark-color-scheme); +} + +.always-visible, +.always-visible .tsd-signatures { + display: inherit !important; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 1.2; +} + +h1 { + font-size: 1.875rem; + margin: 0.67rem 0; +} + +h2 { + font-size: 1.5rem; + margin: 0.83rem 0; +} + +h3 { + font-size: 1.25rem; + margin: 1rem 0; +} + +h4 { + font-size: 1.05rem; + margin: 1.33rem 0; +} + +h5 { + font-size: 1rem; + margin: 1.5rem 0; +} + +h6 { + font-size: 0.875rem; + margin: 2.33rem 0; +} + +.uppercase { + text-transform: uppercase; +} + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +.container { + max-width: 1600px; + padding: 0 2rem; +} + +@media (min-width: 640px) { + .container { + padding: 0 4rem; + } +} +@media (min-width: 1200px) { + .container { + padding: 0 8rem; + } +} +@media (min-width: 1600px) { + .container { + padding: 0 12rem; + } +} + +/* Footer */ +.tsd-generator { + border-top: 1px solid var(--color-accent); + padding-top: 1rem; + padding-bottom: 1rem; + max-height: 3.5rem; +} + +.tsd-generator > p { + margin-top: 0; + margin-bottom: 0; + padding: 0 1rem; +} + +.container-main { + display: flex; + justify-content: space-between; + position: relative; + margin: 0 auto; +} + +.col-4, +.col-8 { + box-sizing: border-box; + float: left; + padding: 2rem 1rem; +} + +.col-4 { + flex: 0 0 25%; +} +.col-8 { + flex: 1 0; + flex-wrap: wrap; + padding-left: 0; +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes fade-out { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + } +} +@keyframes fade-in-delayed { + 0% { + opacity: 0; + } + 33% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fade-out-delayed { + 0% { + opacity: 1; + visibility: visible; + } + 66% { + opacity: 0; + } + 100% { + opacity: 0; + } +} +@keyframes shift-to-left { + from { + transform: translate(0, 0); + } + to { + transform: translate(-25%, 0); + } +} +@keyframes unshift-to-left { + from { + transform: translate(-25%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-in-from-right { + from { + transform: translate(100%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-out-to-right { + from { + transform: translate(0, 0); + visibility: visible; + } + to { + transform: translate(100%, 0); + } +} +body { + background: var(--color-background); + font-family: "Segoe UI", sans-serif; + font-size: 16px; + color: var(--color-text); +} + +a { + color: var(--color-link); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +a.external[target="_blank"] { + background-image: var(--external-icon); + background-position: top 3px right; + background-repeat: no-repeat; + padding-right: 13px; +} + +code, +pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + padding: 0.2em; + margin: 0; + font-size: 0.875rem; + border-radius: 0.8em; +} + +pre { + padding: 10px; + border: 0.1em solid var(--color-accent); +} +pre code { + padding: 0; + font-size: 100%; +} + +blockquote { + margin: 1em 0; + padding-left: 1em; + border-left: 4px solid gray; +} + +.tsd-typography { + line-height: 1.333em; +} +.tsd-typography ul { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-typography h4, +.tsd-typography .tsd-index-panel h3, +.tsd-index-panel .tsd-typography h3, +.tsd-typography h5, +.tsd-typography h6 { + font-size: 1em; + margin: 0; +} +.tsd-typography h5, +.tsd-typography h6 { + font-weight: normal; +} +.tsd-typography p, +.tsd-typography ul, +.tsd-typography ol { + margin: 1em 0; +} + +@media (max-width: 1024px) { + html .col-content { + float: none; + max-width: 100%; + width: 100%; + padding-top: 3rem; + } + html .col-menu { + position: fixed !important; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + z-index: 1024; + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + padding: 1.5rem 1.5rem 0 0; + max-width: 25rem; + visibility: hidden; + background-color: var(--color-background); + transform: translate(100%, 0); + } + html .col-menu > *:last-child { + padding-bottom: 20px; + } + html .overlay { + content: ""; + display: block; + position: fixed; + z-index: 1023; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + visibility: hidden; + } + + .to-has-menu .overlay { + animation: fade-in 0.4s; + } + + .to-has-menu :is(header, footer, .col-content) { + animation: shift-to-left 0.4s; + } + + .to-has-menu .col-menu { + animation: pop-in-from-right 0.4s; + } + + .from-has-menu .overlay { + animation: fade-out 0.4s; + } + + .from-has-menu :is(header, footer, .col-content) { + animation: unshift-to-left 0.4s; + } + + .from-has-menu .col-menu { + animation: pop-out-to-right 0.4s; + } + + .has-menu body { + overflow: hidden; + } + .has-menu .overlay { + visibility: visible; + } + .has-menu :is(header, footer, .col-content) { + transform: translate(-25%, 0); + } + .has-menu .col-menu { + visibility: visible; + transform: translate(0, 0); + display: flex; + flex-direction: column; + gap: 1.5rem; + max-height: 100vh; + padding: 1rem 2rem; + } + .has-menu .tsd-navigation { + max-height: 100%; + } +} + +.tsd-breadcrumb { + margin: 0; + padding: 0; + color: var(--color-text-aside); +} +.tsd-breadcrumb a { + color: var(--color-text-aside); + text-decoration: none; +} +.tsd-breadcrumb a:hover { + text-decoration: underline; +} +.tsd-breadcrumb li { + display: inline; +} +.tsd-breadcrumb li:after { + content: " / "; +} + +.tsd-comment-tags { + display: flex; + flex-direction: column; +} +dl.tsd-comment-tag-group { + display: flex; + align-items: center; + overflow: hidden; + margin: 0.5em 0; +} +dl.tsd-comment-tag-group dt { + display: flex; + margin-right: 0.5em; + font-size: 0.875em; + font-weight: normal; +} +dl.tsd-comment-tag-group dd { + margin: 0; +} +code.tsd-tag { + padding: 0.25em 0.4em; + border: 0.1em solid var(--color-accent); + margin-right: 0.25em; + font-size: 70%; +} +h1 code.tsd-tag:first-of-type { + margin-left: 0.25em; +} + +dl.tsd-comment-tag-group dd:before, +dl.tsd-comment-tag-group dd:after { + content: " "; +} +dl.tsd-comment-tag-group dd pre, +dl.tsd-comment-tag-group dd:after { + clear: both; +} +dl.tsd-comment-tag-group p { + margin: 0; +} + +.tsd-panel.tsd-comment .lead { + font-size: 1.1em; + line-height: 1.333em; + margin-bottom: 2em; +} +.tsd-panel.tsd-comment .lead:last-child { + margin-bottom: 0; +} + +.tsd-filter-visibility h4 { + font-size: 1rem; + padding-top: 0.75rem; + padding-bottom: 0.5rem; + margin: 0; +} +.tsd-filter-item:not(:last-child) { + margin-bottom: 0.5rem; +} +.tsd-filter-input { + display: flex; + width: fit-content; + width: -moz-fit-content; + align-items: center; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + cursor: pointer; +} +.tsd-filter-input input[type="checkbox"] { + cursor: pointer; + position: absolute; + width: 1.5em; + height: 1.5em; + opacity: 0; +} +.tsd-filter-input input[type="checkbox"]:disabled { + pointer-events: none; +} +.tsd-filter-input svg { + cursor: pointer; + width: 1.5em; + height: 1.5em; + margin-right: 0.5em; + border-radius: 0.33em; + /* Leaving this at full opacity breaks event listeners on Firefox. + Don't remove unless you know what you're doing. */ + opacity: 0.99; +} +.tsd-filter-input input[type="checkbox"]:focus + svg { + transform: scale(0.95); +} +.tsd-filter-input input[type="checkbox"]:focus:not(:focus-visible) + svg { + transform: scale(1); +} +.tsd-checkbox-background { + fill: var(--color-accent); +} +input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { + stroke: var(--color-text); +} +.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { + fill: var(--color-background); + stroke: var(--color-accent); + stroke-width: 0.25rem; +} +.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { + stroke: var(--color-accent); +} + +.tsd-theme-toggle { + padding-top: 0.75rem; +} +.tsd-theme-toggle > h4 { + display: inline; + vertical-align: middle; + margin-right: 0.75rem; +} + +.tsd-hierarchy { + list-style: square; + margin: 0; +} +.tsd-hierarchy .target { + font-weight: bold; +} + +.tsd-panel-group.tsd-index-group { + margin-bottom: 0; +} +.tsd-index-panel .tsd-index-list { + list-style: none; + line-height: 1.333em; + margin: 0; + padding: 0.25rem 0 0 0; + overflow: hidden; + display: grid; + grid-template-columns: repeat(3, 1fr); + column-gap: 1rem; + grid-template-rows: auto; +} +@media (max-width: 1024px) { + .tsd-index-panel .tsd-index-list { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 768px) { + .tsd-index-panel .tsd-index-list { + grid-template-columns: repeat(1, 1fr); + } +} +.tsd-index-panel .tsd-index-list li { + -webkit-page-break-inside: avoid; + -moz-page-break-inside: avoid; + -ms-page-break-inside: avoid; + -o-page-break-inside: avoid; + page-break-inside: avoid; +} +.tsd-index-panel a, +.tsd-index-panel a.tsd-parent-kind-module { + color: var(--color-ts); +} +.tsd-index-panel a.tsd-parent-kind-interface { + color: var(--color-ts-interface); +} +.tsd-index-panel a.tsd-parent-kind-enum { + color: var(--color-ts-enum); +} +.tsd-index-panel a.tsd-parent-kind-class { + color: var(--color-ts-class); +} +.tsd-index-panel a.tsd-kind-module { + color: var(--color-ts-namespace); +} +.tsd-index-panel a.tsd-kind-interface { + color: var(--color-ts-interface); +} +.tsd-index-panel a.tsd-kind-enum { + color: var(--color-ts-enum); +} +.tsd-index-panel a.tsd-kind-class { + color: var(--color-ts-class); +} +.tsd-index-panel a.tsd-kind-function { + color: var(--color-ts-function); +} +.tsd-index-panel a.tsd-kind-namespace { + color: var(--color-ts-namespace); +} +.tsd-index-panel a.tsd-kind-variable { + color: var(--color-ts-variable); +} +.tsd-index-panel a.tsd-is-private { + color: var(--color-ts-private); +} + +.tsd-flag { + display: inline-block; + padding: 0.25em 0.4em; + border-radius: 4px; + color: var(--color-comment-tag-text); + background-color: var(--color-comment-tag); + text-indent: 0; + font-size: 75%; + line-height: 1; + font-weight: normal; +} + +.tsd-anchor { + position: absolute; + top: -100px; +} + +.tsd-member { + position: relative; +} +.tsd-member .tsd-anchor + h3 { + display: flex; + align-items: center; + margin-top: 0; + margin-bottom: 0; + border-bottom: none; +} +.tsd-member [data-tsd-kind] { + color: var(--color-ts); +} +.tsd-member [data-tsd-kind="Interface"] { + color: var(--color-ts-interface); +} +.tsd-member [data-tsd-kind="Enum"] { + color: var(--color-ts-enum); +} +.tsd-member [data-tsd-kind="Class"] { + color: var(--color-ts-class); +} +.tsd-member [data-tsd-kind="Private"] { + color: var(--color-ts-private); +} + +.tsd-navigation a { + display: block; + margin: 0.4rem 0; + border-left: 2px solid transparent; + color: var(--color-text); + text-decoration: none; + transition: border-left-color 0.1s; +} +.tsd-navigation a:hover { + text-decoration: underline; +} +.tsd-navigation ul { + margin: 0; + padding: 0; + list-style: none; +} +.tsd-navigation li { + padding: 0; +} + +.tsd-navigation.primary .tsd-accordion-details > ul { + margin-top: 0.75rem; +} +.tsd-navigation.primary a { + padding: 0.75rem 0.5rem; + margin: 0; +} +.tsd-navigation.primary ul li a { + margin-left: 0.5rem; +} +.tsd-navigation.primary ul li li a { + margin-left: 1.5rem; +} +.tsd-navigation.primary ul li li li a { + margin-left: 2.5rem; +} +.tsd-navigation.primary ul li li li li a { + margin-left: 3.5rem; +} +.tsd-navigation.primary ul li li li li li a { + margin-left: 4.5rem; +} +.tsd-navigation.primary ul li li li li li li a { + margin-left: 5.5rem; +} +.tsd-navigation.primary li.current > a { + border-left: 0.15rem var(--color-text) solid; +} +.tsd-navigation.primary li.selected > a { + font-weight: bold; + border-left: 0.2rem var(--color-text) solid; +} +.tsd-navigation.primary ul li a:hover { + border-left: 0.2rem var(--color-text-aside) solid; +} +.tsd-navigation.primary li.globals + li > span, +.tsd-navigation.primary li.globals + li > a { + padding-top: 20px; +} + +.tsd-navigation.secondary.tsd-navigation--toolbar-hide { + max-height: calc(100vh - 1rem); + top: 0.5rem; +} +.tsd-navigation.secondary > ul { + display: inline; + padding-right: 0.5rem; + transition: opacity 0.2s; +} +.tsd-navigation.secondary ul li a { + padding-left: 0; +} +.tsd-navigation.secondary ul li li a { + padding-left: 1.1rem; +} +.tsd-navigation.secondary ul li li li a { + padding-left: 2.2rem; +} +.tsd-navigation.secondary ul li li li li a { + padding-left: 3.3rem; +} +.tsd-navigation.secondary ul li li li li li a { + padding-left: 4.4rem; +} +.tsd-navigation.secondary ul li li li li li li a { + padding-left: 5.5rem; +} + +#tsd-sidebar-links a { + margin-top: 0; + margin-bottom: 0.5rem; + line-height: 1.25rem; +} +#tsd-sidebar-links a:last-of-type { + margin-bottom: 0; +} + +a.tsd-index-link { + margin: 0.25rem 0; + font-size: 1rem; + line-height: 1.25rem; + display: inline-flex; + align-items: center; +} +.tsd-accordion-summary > h1, +.tsd-accordion-summary > h2, +.tsd-accordion-summary > h3, +.tsd-accordion-summary > h4, +.tsd-accordion-summary > h5 { + display: inline-flex; + align-items: center; + vertical-align: middle; + margin-bottom: 0; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} +.tsd-accordion-summary { + display: block; + cursor: pointer; +} +.tsd-accordion-summary > * { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} +.tsd-accordion-summary::-webkit-details-marker { + display: none; +} +.tsd-index-accordion .tsd-accordion-summary svg { + margin-right: 0.25rem; +} +.tsd-index-content > :not(:first-child) { + margin-top: 0.75rem; +} +.tsd-index-heading { + margin-top: 1.5rem; + margin-bottom: 0.75rem; +} + +.tsd-kind-icon { + margin-right: 0.5rem; + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + min-height: 1.25rem; +} +.tsd-kind-icon path { + transform-origin: center; + transform: scale(1.1); +} +.tsd-signature > .tsd-kind-icon { + margin-right: 0.8rem; +} + +@media (min-width: 1025px) { + .col-content { + margin: 2rem auto; + } + + .menu-sticky-wrap { + position: sticky; + height: calc(100vh - 2rem); + top: 4rem; + right: 0; + padding: 0 1.5rem; + padding-top: 1rem; + margin-top: 3rem; + transition: 0.3s ease-in-out; + transition-property: top, padding-top, padding, height; + overflow-y: auto; + } + .col-menu { + border-left: 1px solid var(--color-accent); + } + .col-menu--hide { + top: 1rem; + } + .col-menu .tsd-navigation:not(:last-child) { + padding-bottom: 1.75rem; + } +} + +.tsd-panel { + margin-bottom: 2.5rem; +} +.tsd-panel.tsd-member { + margin-bottom: 4rem; +} +.tsd-panel:empty { + display: none; +} +.tsd-panel > h1, +.tsd-panel > h2, +.tsd-panel > h3 { + margin: 1.5rem -1.5rem 0.75rem -1.5rem; + padding: 0 1.5rem 0.75rem 1.5rem; +} +.tsd-panel > h1.tsd-before-signature, +.tsd-panel > h2.tsd-before-signature, +.tsd-panel > h3.tsd-before-signature { + margin-bottom: 0; + border-bottom: none; +} + +.tsd-panel-group { + margin: 4rem 0; +} +.tsd-panel-group.tsd-index-group { + margin: 2rem 0; +} +.tsd-panel-group.tsd-index-group details { + margin: 2rem 0; +} + +#tsd-search { + transition: background-color 0.2s; +} +#tsd-search .title { + position: relative; + z-index: 2; +} +#tsd-search .field { + position: absolute; + left: 0; + top: 0; + right: 2.5rem; + height: 100%; +} +#tsd-search .field input { + box-sizing: border-box; + position: relative; + top: -50px; + z-index: 1; + width: 100%; + padding: 0 10px; + opacity: 0; + outline: 0; + border: 0; + background: transparent; + color: var(--color-text); +} +#tsd-search .field label { + position: absolute; + overflow: hidden; + right: -40px; +} +#tsd-search .field input, +#tsd-search .title, +#tsd-toolbar-links a { + transition: opacity 0.2s; +} +#tsd-search .results { + position: absolute; + visibility: hidden; + top: 40px; + width: 100%; + margin: 0; + padding: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +#tsd-search .results li { + padding: 0 10px; + background-color: var(--color-background); +} +#tsd-search .results li:nth-child(even) { + background-color: var(--color-background-secondary); +} +#tsd-search .results li.state { + display: none; +} +#tsd-search .results li.current, +#tsd-search .results li:hover { + background-color: var(--color-accent); +} +#tsd-search .results a { + display: block; +} +#tsd-search .results a:before { + top: 10px; +} +#tsd-search .results span.parent { + color: var(--color-text-aside); + font-weight: normal; +} +#tsd-search.has-focus { + background-color: var(--color-accent); +} +#tsd-search.has-focus .field input { + top: 0; + opacity: 1; +} +#tsd-search.has-focus .title, +#tsd-search.has-focus #tsd-toolbar-links a { + z-index: 0; + opacity: 0; +} +#tsd-search.has-focus .results { + visibility: visible; +} +#tsd-search.loading .results li.state.loading { + display: block; +} +#tsd-search.failure .results li.state.failure { + display: block; +} + +#tsd-toolbar-links { + position: absolute; + top: 0; + right: 2rem; + height: 100%; + display: flex; + align-items: center; + justify-content: flex-end; +} +#tsd-toolbar-links a { + margin-left: 1.5rem; +} +#tsd-toolbar-links a:hover { + text-decoration: underline; +} + +.tsd-signature { + margin: 0 0 1rem 0; + padding: 1rem 0.5rem; + border: 1px solid var(--color-accent); + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 14px; + overflow-x: auto; +} + +.tsd-signature-symbol { + color: var(--color-text-aside); + font-weight: normal; +} + +.tsd-signature-type { + font-style: italic; + font-weight: normal; +} + +.tsd-signatures { + padding: 0; + margin: 0 0 1em 0; + list-style-type: none; +} +.tsd-signatures .tsd-signature { + margin: 0; + border-color: var(--color-accent); + border-width: 1px 0; + transition: background-color 0.1s; +} +.tsd-description .tsd-signatures .tsd-signature { + border-width: 1px; +} + +ul.tsd-parameter-list, +ul.tsd-type-parameter-list { + list-style: square; + margin: 0; + padding-left: 20px; +} +ul.tsd-parameter-list > li.tsd-parameter-signature, +ul.tsd-type-parameter-list > li.tsd-parameter-signature { + list-style: none; + margin-left: -20px; +} +ul.tsd-parameter-list h5, +ul.tsd-type-parameter-list h5 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} +.tsd-sources { + margin-top: 1rem; + font-size: 0.875em; +} +.tsd-sources a { + color: var(--color-text-aside); + text-decoration: underline; +} +.tsd-sources ul { + list-style: none; + padding: 0; +} + +.tsd-page-toolbar { + position: fixed; + z-index: 1; + top: 0; + left: 0; + width: 100%; + color: var(--color-text); + background: var(--color-background-secondary); + border-bottom: 1px var(--color-accent) solid; + transition: transform 0.3s ease-in-out; +} +.tsd-page-toolbar a { + color: var(--color-text); + text-decoration: none; +} +.tsd-page-toolbar a.title { + font-weight: bold; +} +.tsd-page-toolbar a.title:hover { + text-decoration: underline; +} +.tsd-page-toolbar .tsd-toolbar-contents { + display: flex; + justify-content: space-between; + height: 2.5rem; + margin: 0 auto; +} +.tsd-page-toolbar .table-cell { + position: relative; + white-space: nowrap; + line-height: 40px; +} +.tsd-page-toolbar .table-cell:first-child { + width: 100%; +} +.tsd-page-toolbar .tsd-toolbar-icon { + box-sizing: border-box; + line-height: 0; + padding: 12px 0; +} + +.tsd-page-toolbar--hide { + transform: translateY(-100%); +} + +.tsd-widget { + display: inline-block; + overflow: hidden; + opacity: 0.8; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-widget:hover { + opacity: 0.9; +} +.tsd-widget.active { + opacity: 1; + background-color: var(--color-accent); +} +.tsd-widget.no-caption { + width: 40px; +} +.tsd-widget.no-caption:before { + margin: 0; +} + +.tsd-widget.options, +.tsd-widget.menu { + display: none; +} +@media (max-width: 1024px) { + .tsd-widget.options, + .tsd-widget.menu { + display: inline-block; + } +} +input[type="checkbox"] + .tsd-widget:before { + background-position: -120px 0; +} +input[type="checkbox"]:checked + .tsd-widget:before { + background-position: -160px 0; +} + +img { + max-width: 100%; +} + +.tsd-anchor-icon { + display: inline-flex; + align-items: center; + margin-left: 0.5rem; + vertical-align: middle; + color: var(--color-text); +} + +.tsd-anchor-icon svg { + width: 1em; + height: 1em; + visibility: hidden; +} + +.tsd-anchor-link:hover > .tsd-anchor-icon svg { + visibility: visible; +} + +.deprecated { + text-decoration: line-through; +} + +.warning { + padding: 1rem; + color: var(--color-warning-text); + background: var(--color-background-warning); +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--color-accent) var(--color-icon-background); +} + +*::-webkit-scrollbar { + width: 0.75rem; +} + +*::-webkit-scrollbar-track { + background: var(--color-icon-background); +} + +*::-webkit-scrollbar-thumb { + background-color: var(--color-accent); + border-radius: 999rem; + border: 0.25rem solid var(--color-icon-background); +} diff --git a/docs/classes/Library.html b/docs/classes/Library.html new file mode 100644 index 00000000..62b5fa1e --- /dev/null +++ b/docs/classes/Library.html @@ -0,0 +1,88 @@ +Library | @matrixai/typescript-demo-lib
+
+ +
+
+
+ +
+

Hierarchy

+
    +
  • Library
+
+
+
+ +
+
+

Constructors

+
+
+

Properties

+
+
+

Constructors

+
+ +
+
+

Properties

+
+ +
someParam: string
+
+
+

Generated using TypeDoc

+
\ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..f283cc44 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,171 @@ +@matrixai/typescript-demo-lib
+
+ +
+
+
+
+

@matrixai/typescript-demo-lib

+
+ +

TypeScript-Demo-Lib

+
+

staging:pipeline status +master:pipeline status

+ + +

Installation

+
+

Note that JavaScript libraries are not packaged in Nix. Only JavaScript applications are.

+

Building the package:

+
nix-build -E '(import ./pkgs.nix {}).callPackage ./default.nix {}'
+
+

Building the releases:

+
nix-build ./release.nix --attr application
nix-build ./release.nix --attr docker
nix-build ./release.nix --attr package.linux.x64.elf
nix-build ./release.nix --attr package.windows.x64.exe
nix-build ./release.nix --attr package.macos.x64.macho +
+

Install into Nix user profile:

+
nix-env -f ./release.nix --install --attr application
+
+

Install into Docker:

+
loaded="$(docker load --input "$(nix-build ./release.nix --attr docker)")"
image="$(cut -d' ' -f3 <<< "$loaded")"
docker run -it "$image" +
+ + +

Development

+
+

Run nix-shell, and once you're inside, you can use:

+
# install (or reinstall packages from package.json)
npm install
# build the dist
npm run build
# run the repl (this allows you to import from ./src)
npm run ts-node
# run the tests
npm run test
# lint the source code
npm run lint
# automatically fix the source
npm run lintfix +
+ + +

Calling Executables

+
+

When calling executables in development, use this style:

+
npm run typescript-demo-lib -- p1 p2 p3
+
+

The -- is necessary to make npm understand that the parameters are for your own executable, and not parameters to npm.

+ + +

Using the REPL

+
+
$ npm run ts-node
> import fs from 'fs';
> fs
> import { Library } from '@';
> Library
> import Library as Library2 from './src/lib/Library'; +
+

You can also create test files in ./src, and run them with npm run ts-node ./src/test.ts.

+

This allows you to test individual pieces of typescript code, and it makes it easier when doing large scale architecting of TypeScript code.

+ + +

Path Aliases

+
+

Due to https://github.com/microsoft/TypeScript/issues/10866, you cannot use path aliases without a bundler like Webpack to further transform the generated JavaScript code in order to resolve the path aliases. Because this is a simple library demonstration, there's no need to use a bundler. In fact, for such libraries, it is far more efficient to not bundle the code.

+

However, we have left the path alias configuration in tsconfig.json, jest.config.js and in the tests we are making use of the @ alias.

+ + +

Local Package Linking

+
+

When developing on multiple NPM packages, it can be easier to use npm link so that changes are immediately reflected rather than repeatedly publishing packages. To do this, you need to use npm link. After linking a local directory, you need to provide tsconfig.json paths so TypeScript compiler can find the right files.

+

For example when linking @matrixai/db located in ../js-db:

+
npm link ../js-db
+
+

You would need to add these paths to tsconfig.json:

+
  "paths": {
"@": ["index"],
"@/*": ["*"],
"@matrixai/db": ["../node_modules/@matrixai/db/src"],
"@matrixai/db/*": ["../node_modules/@matrixai/db/src/*"]
}, +
+ + +

Native Module Toolchain

+
+

There are some nuances when packaging with native modules. +Included native modules are level witch include leveldown and utp-native.

+

If a module is not set to public then pkg defaults to including it as bytecode. +To avoid this breaking with the --no-bytecode flag we need to add --public-packages "*"

+ + +

leveldown

+
+

To get leveldown to work with pkg we need to include the prebuilds with the executable. +after building with pkg you need to copy from node_modules/leveldown/prebuilds -> path_to_executable/prebuilds +You only need to include the prebuilds for the arch you are targeting. e.g. for linux-x64 you need prebuild/linux-x64.

+

The folder structure for the executable should look like this.

+
    +
  • linux_executable_elf
  • +
  • prebuilds
      +
    • linux-x64
        +
      • (node files)
      • +
      +
    • +
    +
  • +
+ + +

utp-native

+
+

Including utp-native is simpler, you just need to add it as an asset for pkg. +Add the following lines to the package.json.

+
"pkg": {
"assets": "node_modules/utp-native/**/*"
} +
+ + +

threads.js

+
+

To make sure that the worker threads work properly you need to include the compiled worker scripts as an asset. +This can be fixed by adding the following to package.json

+
"pkg": {
"assets": "dist/bin/worker.js"
} +
+

If you need to include multiple assets then add them as an array.

+
"pkg": {
"assets": [
"node_modules/utp-native/**/*",
"dist/bin/worker.js"
]
} +
+ + +

Docs Generation

+
+
npm run docs
+
+

See the docs at: https://matrixai.github.io/TypeScript-Demo-Lib/

+ + +

Publishing

+
+

Publishing is handled automatically by the staging pipeline.

+

Prerelease:

+
# npm login
npm version prepatch --preid alpha # premajor/preminor/prepatch
git push --follow-tags +
+

Release:

+
# npm login
npm version patch # major/minor/patch
git push --follow-tags +
+

Manually:

+
# npm login
npm version patch # major/minor/patch
npm run build
npm publish --access public
git push
git push --tags +
+
+
+
+

Generated using TypeDoc

+
\ No newline at end of file diff --git a/docs/modules.html b/docs/modules.html new file mode 100644 index 00000000..d9cf54a5 --- /dev/null +++ b/docs/modules.html @@ -0,0 +1,48 @@ +@matrixai/typescript-demo-lib
+
+ +
+
+
+
+

@matrixai/typescript-demo-lib

+
+
+

Index

+
+

Classes

+
+
+
+

Generated using TypeDoc

+
\ No newline at end of file diff --git a/images/cli_demo.gif b/images/cli_demo.gif new file mode 100644 index 00000000..52a26fc9 Binary files /dev/null and b/images/cli_demo.gif differ diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..ffc4b0e1 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,94 @@ +const os = require('os'); +const path = require('path'); +const fs = require('fs'); +const process = require('process'); +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig'); + +const moduleNameMapper = pathsToModuleNameMapper(compilerOptions.paths, { + prefix: '/src/', +}); + +// Global variables that are shared across the jest worker pool +// These variables must be static and serializable +if ((process.env.PK_TEST_PLATFORM != null) !== (process.env.PK_TEST_COMMAND != null)) throw Error('Both PK_TEST_PLATFORM and PK_TEST_COMMAND must be set together.') +const globals = { + // Absolute directory to the project root + projectDir: __dirname, + // Absolute directory to the test root + testDir: path.join(__dirname, 'tests'), + // Default global data directory + dataDir: fs.mkdtempSync( + path.join(os.tmpdir(), 'polykey-test-global-'), + ), + // Default asynchronous test timeout + defaultTimeout: 20000, + polykeyStartupTimeout: 30000, + failedConnectionTimeout: 50000, + // Timeouts rely on setTimeout which takes 32 bit numbers + maxTimeout: Math.pow(2, 31) - 1, + testCmd: process.env.PK_TEST_COMMAND, + testPlatform: process.env.PK_TEST_PLATFORM, + tmpDir: path.resolve(process.env.PK_TEST_TMPDIR ?? os.tmpdir()), +}; + +// The `globalSetup` and `globalTeardown` cannot access the `globals` +// They run in their own process context +// They can receive process environment +process.env['GLOBAL_DATA_DIR'] = globals.dataDir; + +module.exports = { + testEnvironment: 'node', + verbose: true, + collectCoverage: false, + cacheDirectory: '/tmp/jest', + coverageDirectory: '/tmp/coverage', + roots: ['/tests'], + testMatch: ['**/?(*.)+(spec|test|unit.test).+(ts|tsx|js|jsx)'], + transform: { + "^.+\\.(t|j)sx?$": [ + "@swc/jest", + { + "jsc": { + "parser": { + "syntax": "typescript", + "dynamicImport": true, + "tsx": true, + "decorators": compilerOptions.experimentalDecorators, + }, + "target": compilerOptions.target.toLowerCase(), + "keepClassNames": true, + }, + } + ], + }, + reporters: [ + 'default', + ['jest-junit', { + outputDirectory: '/tmp/junit', + classNameTemplate: '{classname}', + ancestorSeparator: ' > ', + titleTemplate: '{title}', + addFileAttribute: 'true', + reportTestSuiteErrors: 'true', + }], + ], + collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}', '!src/**/*.d.ts'], + coverageReporters: ['text', 'cobertura'], + globals, + // Global setup script executed once before all test files + globalSetup: '/tests/globalSetup.ts', + // Global teardown script executed once after all test files + globalTeardown: '/tests/globalTeardown.ts', + // Setup files are executed before each test file + // Can access globals + setupFiles: ['/tests/setup.ts'], + // Setup files after env are executed before each test file + // after the jest test environment is installed + // Can access globals + setupFilesAfterEnv: [ + 'jest-extended/all', + '/tests/setupAfterEnv.ts' + ], + moduleNameMapper: moduleNameMapper, +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..01ef537a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8642 @@ +{ + "name": "polykey-cli", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "polykey-cli", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@matrixai/errors": "^1.1.7", + "@matrixai/id": "^3.3.6", + "@matrixai/logger": "^3.1.0", + "@matrixai/quic": "^0.0.12", + "commander": "^8.3.0", + "polykey": "^1.1.3-alpha.0", + "threads": "^1.6.5", + "uuid": "^8.3.0" + }, + "bin": { + "pk": "dist/polykey.js", + "polykey": "dist/polykey.js" + }, + "devDependencies": { + "@swc/core": "^1.3.62", + "@swc/jest": "^0.2.26", + "@types/jest": "^28.1.3", + "@types/node": "^18.15.0", + "@typescript-eslint/eslint-plugin": "^5.45.1", + "@typescript-eslint/parser": "^5.45.1", + "eslint": "^8.15.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^28.1.1", + "jest-extended": "^3.0.1", + "jest-junit": "^14.0.0", + "jest-mock-process": "^2.0.0", + "jest-mock-props": "^1.9.1", + "mocked-env": "^1.3.5", + "nexpect": "^0.6.0", + "node-gyp-build": "^4.4.0", + "pkg": "^5.8.1", + "prettier": "^2.6.2", + "shx": "^0.3.4", + "ts-jest": "^28.0.5", + "ts-node": "^10.9.1", + "tsconfig-paths": "^3.9.0", + "typedoc": "^0.23.21", + "typescript": "^4.9.3" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", + "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.7", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/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/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/core": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", + "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", + "dev": true, + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/reporters": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.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": "^28.1.3", + "jest-config": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-regex-util": "^28.0.2", + "jest-resolve": "^28.1.3", + "jest-resolve-dependencies": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "jest-watcher": "^28.1.3", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz", + "integrity": "sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@types/yargs": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", + "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/environment": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", + "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "jest-mock": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz", + "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==", + "dev": true, + "dependencies": { + "expect": "^28.1.3", + "jest-snapshot": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", + "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", + "dev": true, + "dependencies": { + "jest-get-type": "^28.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", + "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.3", + "@sinonjs/fake-timers": "^9.1.2", + "@types/node": "*", + "jest-message-util": "^28.1.3", + "jest-mock": "^28.1.3", + "jest-util": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz", + "integrity": "sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==", + "dev": true, + "dependencies": { + "@jest/environment": "^28.1.3", + "@jest/expect": "^28.1.3", + "@jest/types": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", + "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@jridgewell/trace-mapping": "^0.3.13", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "jest-worker": "^28.1.3", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "28.1.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz", + "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.13", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "dev": true, + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", + "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^28.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", + "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^28.1.3", + "@jridgewell/trace-mapping": "^0.3.13", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "jest-regex-util": "^28.0.2", + "jest-util": "^28.1.3", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^28.1.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": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@matrixai/async-cancellable": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@matrixai/async-cancellable/-/async-cancellable-1.1.1.tgz", + "integrity": "sha512-f0yxu7dHwvffZ++7aCm2WIcCJn18uLcOTdCCwEA3R3KVHYE3TG/JNoTWD9/mqBkAV1AI5vBfJzg27WnF9rOUXQ==" + }, + "node_modules/@matrixai/async-init": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.8.4.tgz", + "integrity": "sha512-33cGC7kHTs9KKwMHJA5d5XURWhx3QUq7lLxPEXLoVfWdTHixcWNvtfshAOso0hbRfx1P3ZSgsb+ZHaIASHhWfg==", + "dependencies": { + "@matrixai/async-locks": "^4.0.0", + "@matrixai/errors": "^1.1.7" + } + }, + "node_modules/@matrixai/async-locks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-locks/-/async-locks-4.0.0.tgz", + "integrity": "sha512-u/3fOdtjOKcDYF8dDoPR1/+7nmOkhxo42eBpXTEgfI0hLPGI37PoW7tjLvwy+O51Quy1HGOwhsR/Dgr4x+euug==", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/errors": "^1.1.7", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.1" + } + }, + "node_modules/@matrixai/contexts": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@matrixai/contexts/-/contexts-1.1.0.tgz", + "integrity": "sha512-sB4UrT8T6OICBujNxTOss8O+dAHnbfndBqZG0fO1PSZUgaZlXDg3cSz9ButbV4JLEz25UvPgh4ChvwTP31DUcQ==", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/async-locks": "^4.0.0", + "@matrixai/errors": "^1.1.7", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.1" + } + }, + "node_modules/@matrixai/db": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@matrixai/db/-/db-5.2.1.tgz", + "integrity": "sha512-pzbzzRSC0r7zgNkNlMEIirIxFsVTUaGrNVhSd/RczoY18WqwaMzCmO/pLAuMX9ML9MD5wAlRUctFz6qIibKybg==", + "hasInstallScript": true, + "dependencies": { + "@matrixai/async-init": "^1.8.4", + "@matrixai/async-locks": "^4.0.0", + "@matrixai/errors": "^1.1.7", + "@matrixai/logger": "^3.1.0", + "@matrixai/resources": "^1.1.5", + "@matrixai/workers": "^1.3.7", + "node-gyp-build": "4.4.0", + "threads": "^1.6.5" + }, + "engines": { + "msvs": "2019", + "node": "^18.15.0" + } + }, + "node_modules/@matrixai/db/node_modules/node-gyp-build": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", + "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/@matrixai/errors": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@matrixai/errors/-/errors-1.1.7.tgz", + "integrity": "sha512-WD6MrlfgtNSTfXt60lbMgwasS5T7bdRgH4eYSOxV+KWngqlkEij9EoDt5LwdvcMD1yuC33DxPTnH4Xu2XV3nMw==", + "dependencies": { + "ts-custom-error": "3.2.2" + } + }, + "node_modules/@matrixai/id": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@matrixai/id/-/id-3.3.6.tgz", + "integrity": "sha512-BpHX/iYxTMuRYtuTzPxKdf6DSwJNVE/EMjLgf/4DSCLGjhT0RQJ8FKKfZReDfb2cx+BsvqL6/LSFM6lfG8v2dw==", + "dependencies": { + "multiformats": "^9.4.8", + "uuid": "^8.3.2" + } + }, + "node_modules/@matrixai/logger": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@matrixai/logger/-/logger-3.1.0.tgz", + "integrity": "sha512-C4JWpgbNik3V99bfGfDell5cH3JULD67eEq9CeXl4rYgsvanF8hhuY84ZYvndPhimt9qjA9/Z8uExKGoiv1zVw==" + }, + "node_modules/@matrixai/quic": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@matrixai/quic/-/quic-0.0.12.tgz", + "integrity": "sha512-aEh21BIgBGqI9IVVAzJF5h/Wu35jmTU35pKLsCJq38wv+1uEbjvWQw3O6oykahHD/2TXwy9ki367lGqOz4ejnw==", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.0", + "@matrixai/async-init": "^1.8.4", + "@matrixai/async-locks": "^4.0.0", + "@matrixai/contexts": "^1.0.0", + "@matrixai/errors": "^1.1.7", + "@matrixai/logger": "^3.1.0", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.1", + "ip-num": "^1.5.0" + }, + "optionalDependencies": { + "@matrixai/quic-darwin-arm64": "0.0.12", + "@matrixai/quic-darwin-x64": "0.0.12", + "@matrixai/quic-linux-x64": "0.0.12", + "@matrixai/quic-win32-x64": "0.0.12" + } + }, + "node_modules/@matrixai/quic-darwin-arm64": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@matrixai/quic-darwin-arm64/-/quic-darwin-arm64-0.0.12.tgz", + "integrity": "sha512-UtoU/xd5H/KP0xlnVO6pGFNj3Lk+IFDnTZXN1Z+kkWHH9fWB4W+z5bQ4f/7ccZ3S+C63o8ROgIkO4/dBB2ajDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@matrixai/quic-darwin-x64": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@matrixai/quic-darwin-x64/-/quic-darwin-x64-0.0.12.tgz", + "integrity": "sha512-HZhjuXcn1OVhBUBS8p6/3wmvr/diJWLVNVjI5T6LByCHBng5ywXBPQYKTOnAoWMtXGUJzFNc4efFfmgpuWTuGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@matrixai/quic-linux-x64": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@matrixai/quic-linux-x64/-/quic-linux-x64-0.0.12.tgz", + "integrity": "sha512-WVS0j0D0UJPGe4q4gVHCppoJ5I6BN4oBTsKKMxMz92g7P9kzx/0JBaEVhHdIJTrxlGDzlKAt9RtZe5hvq/UtpA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@matrixai/resources": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@matrixai/resources/-/resources-1.1.5.tgz", + "integrity": "sha512-m/DEZEe3wHqWEPTyoBtzFF6U9vWYhEnQtGgwvqiAlTxTM0rk96UBpWjDZCTF/vYG11ZlmlQFtg5H+zGgbjaB3Q==" + }, + "node_modules/@matrixai/timer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@matrixai/timer/-/timer-1.1.1.tgz", + "integrity": "sha512-8UKDoGuwKC6BvrY/yANJVH29v71wgQKH/tJlxMPohGxmzVUQO5+JeI4lUYVHTs2vq1AyKAWloF5fOig+I1dyGA==", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/errors": "^1.1.7" + } + }, + "node_modules/@matrixai/workers": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@matrixai/workers/-/workers-1.3.7.tgz", + "integrity": "sha512-37Zm8OqrhLzcPY8uBWBhleN0THOxC49SwtkqTw8Ettb/Csm51MlGoa05NJa0NcBCsYfPucLK/qn1yFIGFrqWhw==", + "dependencies": { + "@matrixai/async-init": "^1.8.4", + "@matrixai/errors": "^1.1.7", + "@matrixai/logger": "^3.1.0", + "threads": "^1.6.5" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.6.tgz", + "integrity": "sha512-Kr0XsyjuElTc4NijuPYyd6YkTlbz0KCuoWnNkfPFhXjHTzbUIh/s15ixjxLj8XDrXsI1aPQp3D64uHbrs3Kuyg==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-x509": "^2.3.6", + "@peculiar/asn1-x509-attr": "^2.3.6", + "asn1js": "^3.0.5", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.6.tgz", + "integrity": "sha512-gCTEB/PvUxapmxo4SzGZT1JtEdevRnphRGZZmc9oJE7+pLuj2Px0Q6x+w8VvObfozA3pyPRTq+Wkocnu64+oLw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-x509": "^2.3.6", + "asn1js": "^3.0.5", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.6.tgz", + "integrity": "sha512-Hu1xzMJQWv8/GvzOiinaE6XiD1/kEhq2C/V89UEoWeZ2fLUcGNIvMxOr/pMyL0OmpRWj/mhCTXOZp4PP+a0aTg==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-x509": "^2.3.6", + "asn1js": "^3.0.5", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.6.tgz", + "integrity": "sha512-bScrrpQ59mppcoZLkDEW/Wruu+daSWQxpR2vqGjg69+v7VoQ1Le/Elm10ObfNShV2eNNridNQcOQvsHMLvUOCg==", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.6", + "@peculiar/asn1-pkcs8": "^2.3.6", + "@peculiar/asn1-rsa": "^2.3.6", + "@peculiar/asn1-schema": "^2.3.6", + "asn1js": "^3.0.5", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.6.tgz", + "integrity": "sha512-poqgdjsHNiyR0gnxP8l5VjRInSgpQvOM3zLULF/ZQW67uUsEiuPfplvaNJUlNqNOCd2szGo9jKW9+JmVVpWojA==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-x509": "^2.3.6", + "asn1js": "^3.0.5", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.6.tgz", + "integrity": "sha512-uaxSBF60glccuu5BEZvoPsaJzebVYcQRjXx2wXsGe7Grz/BXtq5RQAJ/3i9fEXawFK/zIbvbXBBpy07cnvrqhA==", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.6", + "@peculiar/asn1-pfx": "^2.3.6", + "@peculiar/asn1-pkcs8": "^2.3.6", + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-x509": "^2.3.6", + "@peculiar/asn1-x509-attr": "^2.3.6", + "asn1js": "^3.0.5", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.6.tgz", + "integrity": "sha512-DswjJyAXZnvESuImGNTvbNKvh1XApBVqU+r3UmrFFTAI23gv62byl0f5OFKWTNhCf66WQrd3sklpsCZc/4+jwA==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-x509": "^2.3.6", + "asn1js": "^3.0.5", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz", + "integrity": "sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.6.tgz", + "integrity": "sha512-dRwX31R1lcbIdzbztiMvLNTDoGptxdV7HocNx87LfKU0fEWh7fTWJjx4oV+glETSy6heF/hJHB2J4RGB3vVSYg==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "asn1js": "^3.0.5", + "ipaddr.js": "^2.0.1", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.6.tgz", + "integrity": "sha512-x5Kax8xp3fz+JSc+4Sq0/SUXIdbJeOePibYqvjHMGkP6AoeCOVcP+gg7rZRRGkTlDSyQnAoUTgTEsfAfFEd1/g==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-x509": "^2.3.6", + "asn1js": "^3.0.5", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.0.tgz", + "integrity": "sha512-U58N44b2m3OuTgpmKgf0LPDOmP3bhwNz01vAnj1mBwxBASRhptWYK+M3zG+HBkDqGQM+bFsoIihTW8MdmPXEqg==", + "dependencies": { + "@peculiar/asn1-schema": "^2.1.6", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0", + "webcrypto-core": "^1.7.4" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.8.3.tgz", + "integrity": "sha512-omZfI3n4eGLS5NLudURzbc0smQ4ePreOPUEk31n1MLaqd2GGb48b4Zw5xjHzHJ0hnPYmZ+NRjqqquXYUYKjMCw==", + "dependencies": { + "@peculiar/asn1-cms": "^2.2.0", + "@peculiar/asn1-csr": "^2.2.0", + "@peculiar/asn1-ecc": "^2.2.0", + "@peculiar/asn1-pkcs9": "^2.2.0", + "@peculiar/asn1-rsa": "^2.2.0", + "@peculiar/asn1-schema": "^2.2.0", + "@peculiar/asn1-x509": "^2.2.0", + "pvtsutils": "^1.3.2", + "reflect-metadata": "^0.1.13", + "tslib": "^2.4.0", + "tsyringe": "^4.7.0" + } + }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@streamparser/json": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.13.tgz", + "integrity": "sha512-buWyDbFht82G2Dgt8yS1AiR12Y7uvgQv+wOY6X98Pattq95RyJp7wJp0zxDdI/6jqKSlHKwGJNX7KjrSnYHbOQ==" + }, + "node_modules/@swc/core": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.74.tgz", + "integrity": "sha512-P+MIExOTdWlfq8Heb1/NhBAke6UTckd4cRDuJoFcFMGBRvgoCMNWhnfP3FRRXPLI7GGg27dRZS+xHiqYyQmSrA==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.3.74", + "@swc/core-darwin-x64": "1.3.74", + "@swc/core-linux-arm-gnueabihf": "1.3.74", + "@swc/core-linux-arm64-gnu": "1.3.74", + "@swc/core-linux-arm64-musl": "1.3.74", + "@swc/core-linux-x64-gnu": "1.3.74", + "@swc/core-linux-x64-musl": "1.3.74", + "@swc/core-win32-arm64-msvc": "1.3.74", + "@swc/core-win32-ia32-msvc": "1.3.74", + "@swc/core-win32-x64-msvc": "1.3.74" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.74.tgz", + "integrity": "sha512-2rMV4QxM583jXcREfo0MhV3Oj5pgRSfSh/kVrB1twL2rQxOrbzkAPT/8flmygdVoL4f2F7o1EY5lKlYxEBiIKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.74.tgz", + "integrity": "sha512-KKEGE1wXneYXe15fWDRM8/oekd/Q4yAuccA0vWY/7i6nOSPqWYcSDR0nRtR030ltDxWt0rk/eCTmNkrOWrKs3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.74.tgz", + "integrity": "sha512-HehH5DR6r/5fIVu7tu8ZqgrHkhSCQNewf1ztFQJgcmaQWn+H4AJERBjwkjosqh4TvUJucZv8vyRTvrFeBXaCSA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.74.tgz", + "integrity": "sha512-+xkbCRz/wczgdknoV4NwYxbRI2dD7x/qkIFcVM2buzLCq8oWLweuV8+aL4pRqu0qDh7ZSb1jcaVTUIsySCJznA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.74.tgz", + "integrity": "sha512-maKFZSCD3tQznzPV7T3V+TtiWZFEFM8YrnSS5fQNNb+K9J65sL+170uTb3M7H4cFkG+9Sm5k5yCrCIutlvV48g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.74.tgz", + "integrity": "sha512-LEXpcShF6DLTWJSiBhMSYZkLQ27UvaQ24fCFhoIV/R3dhYaUpHmIyLPPBNC82T03lB3ONUFVwrRw6fxDJ/f00A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.74.tgz", + "integrity": "sha512-sxsFctbFMZEFmDE7CmYljG0dMumH8XBTwwtGr8s6z0fYAzXBGNq2AFPcmEh2np9rPWkt7pE1m0ByESD+dMkbxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.74.tgz", + "integrity": "sha512-F7hY9/BjFCozA4YPFYFH5FGCyWwa44vIXHqG66F5cDwXDGFn8ZtBsYIsiPfUYcx0AeAo1ojnVWKPxokZhYNYqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.74.tgz", + "integrity": "sha512-qBAsiD1AlIdqED6wy3UNRHyAys9pWMUidX0LJ6mj24r/vfrzzTBAUrLJe5m7bzE+F1Rgi001avYJeEW1DLEJ+Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.3.74", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.74.tgz", + "integrity": "sha512-S3YAvvLprTnPRwQuy9Dkwubb5SRLpVK3JJsqYDbGfgj8PGQyKHZcVJ5X3nfFsoWLy3j9B/3Os2nawprRSzeC5A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/jest": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.28.tgz", + "integrity": "sha512-iCB3lvngkQldLga35krb8LPa+6gmkVXnlpfCTXOAgMaEYFagLxOIFbIO8II7dhHa8ApOv5ap8iFRETI4lVY0vw==", + "dev": true, + "dependencies": { + "@jest/create-cache-key-function": "^27.4.2", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "28.1.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.8.tgz", + "integrity": "sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw==", + "dev": true, + "dependencies": { + "expect": "^28.0.0", + "pretty-format": "^28.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.17.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.3.tgz", + "integrity": "sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA==" + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", + "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", + "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", + "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", + "dev": true, + "dependencies": { + "@jest/transform": "^28.1.3", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^28.1.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", + "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", + "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^28.1.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/bitset": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bitset/-/bitset-5.1.1.tgz", + "integrity": "sha512-oKaRp6mzXedJ1Npo86PKhWfDelI6HxxJo+it9nAcBB0HLVvYVp+5i6yj6DT5hfFgo+TS5T57MRWtw8zhwdTs3g==", + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001519", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", + "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==", + "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/canonicalize": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.8.tgz", + "integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "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==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.485", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.485.tgz", + "integrity": "sha512-1ndQ5IBNEnFirPwvyud69GHL+31FkE09gH/CJ6m3KCbkx3i0EVOrjwz4UNxRmN9H8OVHbC6vMRZGN1yCvjSs9w==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encryptedfs": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/encryptedfs/-/encryptedfs-3.5.8.tgz", + "integrity": "sha512-NDTQvfeLfWGbgq5ceJwyE4fiwE1z1F5eNI6U7iMYDJLvYblEFCJ8B4x9xOR/vknBCQ3CrrvGVvamX8NPCnMOPA==", + "dependencies": { + "@matrixai/async-init": "^1.8.4", + "@matrixai/async-locks": "^4.0.0", + "@matrixai/db": "^5.2.0", + "@matrixai/errors": "^1.1.7", + "@matrixai/logger": "^3.1.0", + "@matrixai/resources": "^1.1.5", + "@matrixai/workers": "^1.3.7", + "errno": "^0.1.7", + "lexicographic-integer": "^1.1.0", + "node-forge": "^1.3.1", + "readable-stream": "^3.6.0", + "resource-counter": "^1.2.4", + "threads": "^1.6.5", + "util-callbackify": "^1.0.0" + } + }, + "node_modules/encryptedfs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.1", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.2", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.8.tgz", + "integrity": "sha512-tEe+Pok22qIGaK3KoMP+N96GVDS66B/zreoVVmiavLvRUEmGRtvb4B8wO9jwnb8d2lvHtrkhZ7UD73dWBVnf/Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz", + "integrity": "sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", + "has": "^1.0.3", + "is-core-module": "^2.12.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "resolve": "^1.22.3", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", + "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^28.1.3", + "jest-get-type": "^28.0.2", + "jest-matcher-utils": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-fuzzy": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/fast-fuzzy/-/fast-fuzzy-1.12.0.tgz", + "integrity": "sha512-sXxGgHS+ubYpsdLnvOvJ9w5GYYZrtL9mkosG3nfuD446ahvoWEsSKBP7ieGmWIKVLnaxRDgUJkZMdxRgA2Ni+Q==", + "dependencies": { + "graphemesplit": "^2.4.1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "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-glob/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/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==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-lock": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-1.2.0.tgz", + "integrity": "sha512-Lk/pKH2DldLpG4Yh/sOOY84k5VqNzxHPffGwf1+yYI+/qMXzTPp9KJMX+Wh6n4xqGSA1Mu7JPmaDArfJGw2O/A==", + "hasInstallScript": true, + "dependencies": { + "napi-macros": "^2.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "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.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "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": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/graphemesplit": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/graphemesplit/-/graphemesplit-2.4.4.tgz", + "integrity": "sha512-lKrpp1mk1NH26USxC/Asw4OHbhSQf5XfrWZ+CDv/dFVvd1j17kFgMotdJvOesmHkbFX9P9sBfpH8VogxOWLg8w==", + "dependencies": { + "js-base64": "^3.6.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-num": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ip-num/-/ip-num-1.5.1.tgz", + "integrity": "sha512-QziFxgxq3mjIf5CuwlzXFYscHxgLqdEdJKRo2UJ5GurL5zrSRMzT/O+nK0ABimoFH8MWF8YwIiwECYsHc1LpUQ==" + }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-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", + "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-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-observable": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", + "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isomorphic-git": { + "version": "1.24.5", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.24.5.tgz", + "integrity": "sha512-07M4YscftHZJIuw7xZhgWkdFvVjHSBJBsIwWXkxgFCivhb0l8mGNchM7nO2hU27EKSIf0sT4gJivEgLGohWbzA==", + "dependencies": { + "async-lock": "^1.1.0", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.9", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/isomorphic-git/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/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-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ix": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ix/-/ix-5.0.0.tgz", + "integrity": "sha512-6LyyrHnvNrSy5pKtW/KA+KKusHrB223aBJCJlIGPN7QBfDkEEtNrAkAz9lLLShIcdJntq6BiPCHuKaCM/9wwXw==", + "dependencies": { + "@types/node": "^13.7.4", + "tslib": "^2.3.0" + } + }, + "node_modules/ix/node_modules/@types/node": { + "version": "13.13.52", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", + "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==" + }, + "node_modules/jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", + "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", + "dev": true, + "dependencies": { + "@jest/core": "^28.1.3", + "@jest/types": "^28.1.3", + "import-local": "^3.0.2", + "jest-cli": "^28.1.3" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.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": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz", + "integrity": "sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-circus": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz", + "integrity": "sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==", + "dev": true, + "dependencies": { + "@jest/environment": "^28.1.3", + "@jest/expect": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^28.1.3", + "jest-matcher-utils": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "p-limit": "^3.1.0", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-cli": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz", + "integrity": "sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==", + "dev": true, + "dependencies": { + "@jest/core": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz", + "integrity": "sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^28.1.3", + "@jest/types": "^28.1.3", + "babel-jest": "^28.1.3", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^28.1.3", + "jest-environment-node": "^28.1.3", + "jest-get-type": "^28.0.2", + "jest-regex-util": "^28.0.2", + "jest-resolve": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.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.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.2.tgz", + "integrity": "sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/@jest/schemas": { + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", + "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/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/jest-diff/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/jest-diff/node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", + "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz", + "integrity": "sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-each": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz", + "integrity": "sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.3", + "chalk": "^4.0.0", + "jest-get-type": "^28.0.2", + "jest-util": "^28.1.3", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz", + "integrity": "sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==", + "dev": true, + "dependencies": { + "@jest/environment": "^28.1.3", + "@jest/fake-timers": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "jest-mock": "^28.1.3", + "jest-util": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-extended": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-3.2.4.tgz", + "integrity": "sha512-lSEYhSmvXZG/7YXI7KO3LpiUiQ90gi5giwCJNDMMsX5a+/NZhdbQF2G4ALOBN+KcXVT3H6FPVPohAuMXooaLTQ==", + "dev": true, + "dependencies": { + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": ">=27.2.5" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, + "node_modules/jest-extended/node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz", + "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.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": "^28.0.2", + "jest-util": "^28.1.3", + "jest-worker": "^28.1.3", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-junit": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-14.0.1.tgz", + "integrity": "sha512-h7/wwzPbllgpQhhVcRzRC76/cc89GlazThoV1fDxcALkf26IIlRsu/AcTG64f4nR2WPE3Cbd+i/sVf+NCUHrWQ==", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz", + "integrity": "sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==", + "dev": true, + "dependencies": { + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", + "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^28.1.3", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-mock": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz", + "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-mock-process": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-mock-process/-/jest-mock-process-2.0.0.tgz", + "integrity": "sha512-bybzszPfvrYhplymvUNFc130ryvjSCW1JSCrLA0LiV0Sv9TrI+cz90n3UYUPoT2nhNL6c6IV9LxUSFJF9L9tHQ==", + "dev": true, + "peerDependencies": { + "jest": ">=23.4" + } + }, + "node_modules/jest-mock-props": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/jest-mock-props/-/jest-mock-props-1.9.1.tgz", + "integrity": "sha512-PvTySOTw/K4dwL7XrVGq/VUZRm/qXPrV4+NuhgxuWkmE3h/Fd+g+qB0evK5vSBAkI8TaxvTXYv17IdxWdEze1g==", + "dev": true, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "jest": ">=24.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": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", + "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz", + "integrity": "sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz", + "integrity": "sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^28.0.2", + "jest-snapshot": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-runner": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz", + "integrity": "sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==", + "dev": true, + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/environment": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "graceful-fs": "^4.2.9", + "jest-docblock": "^28.1.1", + "jest-environment-node": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-leak-detector": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-resolve": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-util": "^28.1.3", + "jest-watcher": "^28.1.3", + "jest-worker": "^28.1.3", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz", + "integrity": "sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==", + "dev": true, + "dependencies": { + "@jest/environment": "^28.1.3", + "@jest/fake-timers": "^28.1.3", + "@jest/globals": "^28.1.3", + "@jest/source-map": "^28.1.2", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-mock": "^28.1.3", + "jest-regex-util": "^28.0.2", + "jest-resolve": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz", + "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^28.1.3", + "graceful-fs": "^4.2.9", + "jest-diff": "^28.1.3", + "jest-get-type": "^28.0.2", + "jest-haste-map": "^28.1.3", + "jest-matcher-utils": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "natural-compare": "^1.4.0", + "pretty-format": "^28.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-validate": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz", + "integrity": "sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^28.0.2", + "leven": "^3.1.0", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.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": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^28.1.3", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.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-base64": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", + "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "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/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "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.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lexicographic-integer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/lexicographic-integer/-/lexicographic-integer-1.1.0.tgz", + "integrity": "sha512-MQCrf1gG31DJSNQDiIfgk7CQVlXkO6xC+DFGExs5WQWlxWSSAroH5k/UrKrS6LThHDHBoc3X1pNoYHDKOCPWRQ==" + }, + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "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/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dependencies": { + "minimist": "^1.2.5" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/mocked-env": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/mocked-env/-/mocked-env-1.3.5.tgz", + "integrity": "sha512-GyYY6ynVOdEoRlaGpaq8UYwdWkvrsU2xRme9B+WPSuJcNjh17+3QIxSYU6zwee0SbehhV6f06VZ4ahjG+9zdrA==", + "dev": true, + "dependencies": { + "check-more-types": "2.24.0", + "debug": "4.3.2", + "lazy-ass": "1.6.0", + "ramda": "0.27.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mocked-env/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==" + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "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": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/multistream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "node_modules/napi-macros": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==" + }, + "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/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/nexpect": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nexpect/-/nexpect-0.6.0.tgz", + "integrity": "sha512-gG4cO0zoNG+kaPesw516hPVEKLW3YizGU8UWMr5lpkHKOgoTWcu4sPQN7rWVAIL4Ck87zM4N8immPUhYPdDz3g==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.5" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/nexpect/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/nexpect/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nexpect/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nexpect/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nexpect/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nexpect/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/node-abi": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "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.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", + "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", + "dependencies": { + "array.prototype.reduce": "^1.0.5", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "safe-array-concat": "^1.0.0" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz", + "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/observable-fns": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz", + "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "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/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "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/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, + "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": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", + "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", + "dev": true, + "dependencies": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "peerDependencies": { + "node-notifier": ">=9.0.1" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "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/pkg-dir/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/pkg-dir/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/pkg-dir/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/pkg-dir/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/pkg-fetch": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", + "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/pkg-fetch/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/pkg-fetch/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pkg-fetch/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/pkg/node_modules/@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/pkg/node_modules/@babel/parser": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pkg/node_modules/@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/pkg/node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/polykey": { + "version": "1.1.3-alpha.0", + "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.1.3-alpha.0.tgz", + "integrity": "sha512-8u9cpd6jPYnHo5IgjmJiwdeOjHIPzGNZIxOQpIbrJzbhrD8PtPSO7QLvMNuCLmlVotecsT5/mEUv2tAZkx1MOQ==", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/async-init": "^1.8.4", + "@matrixai/async-locks": "^4.0.0", + "@matrixai/contexts": "^1.1.0", + "@matrixai/db": "^5.2.0", + "@matrixai/errors": "^1.1.7", + "@matrixai/id": "^3.3.6", + "@matrixai/logger": "^3.1.0", + "@matrixai/quic": "^0.0.13", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.1", + "@matrixai/workers": "^1.3.7", + "@peculiar/asn1-pkcs8": "^2.3.0", + "@peculiar/asn1-schema": "^2.3.0", + "@peculiar/asn1-x509": "^2.3.0", + "@peculiar/webcrypto": "1.4.0", + "@peculiar/x509": "1.8.3", + "@scure/bip39": "^1.1.0", + "@streamparser/json": "^0.0.13", + "@types/ws": "^8.5.4", + "ajv": "^7.0.4", + "canonicalize": "^1.0.5", + "cheerio": "^1.0.0-rc.5", + "commander": "^8.3.0", + "cross-fetch": "^3.0.6", + "cross-spawn": "^7.0.3", + "encryptedfs": "^3.5.6", + "fast-fuzzy": "^1.10.8", + "fd-lock": "^1.2.0", + "ip-num": "^1.3.3-0", + "isomorphic-git": "^1.8.1", + "ix": "^5.0.0", + "lexicographic-integer": "^1.1.0", + "multiformats": "^9.4.8", + "pako": "^1.0.11", + "prompts": "^2.4.1", + "readable-stream": "^3.6.0", + "resource-counter": "^1.2.4", + "sodium-native": "^3.4.1", + "threads": "^1.6.5", + "tslib": "^2.4.0", + "tsyringe": "^4.7.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.19.0", + "ws": "^8.12.0" + } + }, + "node_modules/polykey/node_modules/@matrixai/quic": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@matrixai/quic/-/quic-0.0.13.tgz", + "integrity": "sha512-tvlA0m2fUIchyEZxzkBbvYNXYf21u0gR4Lv2BaYZYmGa1Fr2VH07MCZu9Ka8DpAEOXKEU94yqhNSEKCCJ83LJA==", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.0", + "@matrixai/async-init": "^1.8.4", + "@matrixai/async-locks": "^4.0.0", + "@matrixai/contexts": "^1.0.0", + "@matrixai/errors": "^1.1.7", + "@matrixai/logger": "^3.1.0", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.1", + "ip-num": "^1.5.0" + }, + "optionalDependencies": { + "@matrixai/quic-darwin-arm64": "0.0.13", + "@matrixai/quic-darwin-x64": "0.0.13", + "@matrixai/quic-linux-x64": "0.0.13", + "@matrixai/quic-win32-x64": "0.0.13" + } + }, + "node_modules/polykey/node_modules/@matrixai/quic-darwin-arm64": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@matrixai/quic-darwin-arm64/-/quic-darwin-arm64-0.0.13.tgz", + "integrity": "sha512-EKBfqYr6mMj0k9cE97KiommyFb7eD3u4OWloMFySERcBzg+9HWwonDX5/kyChllxEDorPXneW/CfF8gtZTQ1ug==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/polykey/node_modules/@matrixai/quic-darwin-x64": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@matrixai/quic-darwin-x64/-/quic-darwin-x64-0.0.13.tgz", + "integrity": "sha512-WTf9gKdAqHkWVk48eWZ4JofctjZBrvUxEfg8HcBDzye1kz1O+0IyJlF4web3ZFYu/lvoGQn6DTaJvozdQS5hTw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/polykey/node_modules/@matrixai/quic-linux-x64": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@matrixai/quic-linux-x64/-/quic-linux-x64-0.0.13.tgz", + "integrity": "sha512-ExOhO9YjiCNV6OrRMF2+CVQdPANa2zSqlMzCUaLC5whAsll50M08LpoV4J/HnmpTWPcfohr+G28bFWVsnb8/wA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/polykey/node_modules/ajv": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.2.4.tgz", + "integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/polykey/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/polykey/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.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/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz", + "integrity": "sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, + "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/ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", + "dev": true + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "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-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-cwd/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-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", + "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/resource-counter": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/resource-counter/-/resource-counter-1.2.4.tgz", + "integrity": "sha512-DGJChvE5r4smqPE+xYNv9r1u/I9cCfRR5yfm7D6EQckdKqMyVpJ5z0s40yn0EM0puFxHg6mPORrQLQdEbJ/RnQ==", + "dependencies": { + "babel-runtime": "^6.26.0", + "bitset": "^5.0.3" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "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/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shiki": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.3.tgz", + "integrity": "sha512-U3S/a+b0KS+UkTyMjoNojvTgrBHjgp7L6ovhFVZsXmBGnVdQ4K4U9oK0z63w538S91ATngv1vXigHCSWOwnr+g==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sodium-native": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.4.1.tgz", + "integrity": "sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.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/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.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/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/threads": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz", + "integrity": "sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==", + "dependencies": { + "callsites": "^3.1.0", + "debug": "^4.2.0", + "is-observable": "^2.1.0", + "observable-fns": "^0.6.1" + }, + "funding": { + "url": "https://github.com/andywer/threads.js?sponsor=1" + }, + "optionalDependencies": { + "tiny-worker": ">= 2" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "node_modules/tiny-worker": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", + "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==", + "optional": true, + "dependencies": { + "esm": "^3.2.25" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-custom-error": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.2.tgz", + "integrity": "sha512-u0YCNf2lf6T/vHm+POKZK1yFKWpSpJitcUN3HxqyEcFuNnHIDbyuIQC7QDy/PsBX3giFyk9rt6BFqBAh2lsDZQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-jest": { + "version": "28.0.8", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz", + "integrity": "sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^28.0.0", + "json5": "^2.2.1", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^28.0.0", + "babel-jest": "^28.0.0", + "jest": "^28.0.0", + "typescript": ">=4.3" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsyringe": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz", + "integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "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.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedoc": { + "version": "0.23.28", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", + "integrity": "sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.2.12", + "minimatch": "^7.1.3", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 14.14" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" + } + }, + "node_modules/typedoc/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/typedoc/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/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "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.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-callbackify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util-callbackify/-/util-callbackify-1.0.0.tgz", + "integrity": "sha512-5vEPPSM6DCHlCpq9FZryeIkY5FQMUqXLUz4yHtU369Z/abWUVdgInPVeINjWJV3Bk9DZhCr+JzGarEByPLsxBQ==", + "dependencies": { + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uWebSockets.js": { + "version": "20.19.0", + "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#42c9c0d5d31f46ca4115dc75672b0037ec970f28" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webcrypto-core": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.7.tgz", + "integrity": "sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.1", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "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/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/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/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..296b8ede --- /dev/null +++ b/package.json @@ -0,0 +1,113 @@ +{ + "name": "polykey-cli", + "version": "0.0.1", + "homepage": "https://polykey.io", + "author": "Roger Qiu", + "contributors": [ + { + "name": "Roger Qiu" + }, + { + "name": "Aashwin Varshney" + }, + { + "name": "Robert Cronin" + }, + { + "name": "Lucas Lin" + }, + { + "name": "Gideon Rosales" + }, + { + "name": "Scott Morris" + }, + { + "name": "Joshua Karp" + }, + { + "name": "Brian Botha" + }, + { + "name": "Emma Casolin" + } + ], + "description": "Polykey CLI", + "keywords": [ + "secrets", + "password" + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/MatrixAI/Polykey-CLI.git" + }, + "bin": { + "polykey": "dist/polykey.js", + "pk": "dist/polykey.js" + }, + "pkg": { + "assets": [ + "dist/**/*.json", + "node_modules/tslib/**/*.js", + "node_modules/tsyringe/**/*.js", + "node_modules/uWebSockets.js/**/*.js" + ], + "scripts": [ + "dist/lib/workers/worker.js" + ] + }, + "scripts": { + "prepare": "tsc -p ./tsconfig.build.json", + "build": "shx rm -rf ./dist && tsc -p ./tsconfig.build.json", + "postversion": "npm install --package-lock-only --ignore-scripts --silent", + "ts-node": "ts-node", + "test": "jest", + "lint": "eslint '{src,tests,scripts}/**/*.{js,ts}'", + "lintfix": "eslint '{src,tests,scripts}/**/*.{js,ts}' --fix", + "lint-shell": "find ./src ./tests ./scripts -type f -regextype posix-extended -regex '.*\\.(sh)' -exec shellcheck {} +", + "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", + "pkg": "node ./scripts/pkg.js", + "polykey": "ts-node src/polykey.ts", + "start": "ts-node src/polykey.ts -- agent start --verbose", + "dev": "nodemon src/polykey.ts -- agent start --verbose" + }, + "dependencies": { + "polykey": "^1.1.3-alpha.0", + "@matrixai/logger": "^3.1.0", + "@matrixai/errors": "^1.1.7", + "@matrixai/quic": "^0.0.12", + "@matrixai/id": "^3.3.6", + "threads": "^1.6.5", + "uuid": "^8.3.0", + "commander": "^8.3.0" + }, + "devDependencies": { + "@swc/core": "^1.3.62", + "@swc/jest": "^0.2.26", + "@types/jest": "^28.1.3", + "@types/node": "^18.15.0", + "@typescript-eslint/eslint-plugin": "^5.45.1", + "@typescript-eslint/parser": "^5.45.1", + "eslint": "^8.15.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^28.1.1", + "jest-extended": "^3.0.1", + "jest-junit": "^14.0.0", + "jest-mock-process": "^2.0.0", + "node-gyp-build": "^4.4.0", + "pkg": "^5.8.1", + "prettier": "^2.6.2", + "shx": "^0.3.4", + "ts-jest": "^28.0.5", + "ts-node": "^10.9.1", + "tsconfig-paths": "^3.9.0", + "typedoc": "^0.23.21", + "typescript": "^4.9.3", + "mocked-env": "^1.3.5", + "nexpect": "^0.6.0", + "jest-mock-props": "^1.9.1" + } +} diff --git a/pkgs.nix b/pkgs.nix new file mode 100644 index 00000000..bb501409 --- /dev/null +++ b/pkgs.nix @@ -0,0 +1,4 @@ +import ( + let rev = "f294325aed382b66c7a188482101b0f336d1d7db"; in + builtins.fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz" +) diff --git a/release.nix b/release.nix new file mode 100644 index 00000000..2464011e --- /dev/null +++ b/release.nix @@ -0,0 +1,106 @@ +{ pkgs ? import ./pkgs.nix {} }: + +with pkgs; +let + utils = callPackage ./utils.nix {}; + buildElf = arch: + stdenv.mkDerivation rec { + name = "${utils.basename}-${version}-linux-${arch}"; + version = utils.node2nixDev.version; + src = "${utils.node2nixDev}/lib/node_modules/${utils.node2nixDev.packageName}"; + nativeBuildInputs = [ nodejs ]; + PKG_CACHE_PATH = utils.pkgCachePath; + PKG_IGNORE_TAG = 1; + buildPhase = '' + npm run pkg -- \ + --output=out \ + --bin=polykey \ + --node-version=${utils.nodeVersion} \ + --platform=linux \ + --arch=${arch} + ''; + installPhase = '' + cp out $out + ''; + dontFixup = true; + }; + buildExe = arch: + stdenv.mkDerivation rec { + name = "${utils.basename}-${version}-win-${arch}.exe"; + version = utils.node2nixDev.version; + src = "${utils.node2nixDev}/lib/node_modules/${utils.node2nixDev.packageName}"; + nativeBuildInputs = [ nodejs ]; + PKG_CACHE_PATH = utils.pkgCachePath; + PKG_IGNORE_TAG = 1; + buildPhase = '' + npm run pkg -- \ + --output=out.exe \ + --bin=polykey \ + --node-version=${utils.nodeVersion} \ + --platform=win32 \ + --arch=${arch} + ''; + installPhase = '' + cp out.exe $out + ''; + dontFixup = true; + }; + buildMacho = arch: + stdenv.mkDerivation rec { + name = "${utils.basename}-${version}-macos-${arch}"; + version = utils.node2nixDev.version; + src = "${utils.node2nixDev}/lib/node_modules/${utils.node2nixDev.packageName}"; + nativeBuildInputs = [ nodejs ]; + PKG_CACHE_PATH = utils.pkgCachePath; + PKG_IGNORE_TAG = 1; + buildPhase = '' + npm run pkg -- \ + --output=out \ + --bin=polykey \ + --node-version=${utils.nodeVersion} \ + --platform=darwin \ + --arch=${arch} + ''; + installPhase = '' + cp out $out + ''; + dontFixup = true; + }; +in + rec { + application = callPackage ./default.nix {}; + docker = dockerTools.buildImage { + name = application.name; + contents = [ application ]; + # This ensures symlinks to directories are preserved in the image + keepContentsDirlinks = true; + # This adds a correct timestamp, however breaks binary reproducibility + created = "now"; + extraCommands = '' + mkdir -m 1777 tmp + ''; + config = { + Entrypoint = "polykey"; + }; + }; + package = { + linux = { + x64 = { + elf = buildElf "x64"; + }; + }; + windows = { + x64 = { + exe = buildExe "x64"; + }; + }; + macos = { + x64 = { + macho = buildMacho "x64"; + }; + arm64 = { + macho = buildMacho "arm64"; + }; + }; + }; + } diff --git a/scripts/brew-install.sh b/scripts/brew-install.sh new file mode 100755 index 00000000..11215a63 --- /dev/null +++ b/scripts/brew-install.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -o errexit # abort on nonzero exitstatus +set -o nounset # abort on unbound variable +set -o pipefail # don't hide errors within pipes + +export HOMEBREW_NO_INSTALL_UPGRADE=1 +export HOMEBREW_NO_INSTALL_CLEANUP=1 +export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 +export HOMEBREW_NO_AUTO_UPDATE=1 +export HOMEBREW_NO_ANALYTICS=1 + +brew install node@18 +brew link --overwrite node@18 diff --git a/scripts/build-platforms-generate.sh b/scripts/build-platforms-generate.sh new file mode 100755 index 00000000..7a26c3a1 --- /dev/null +++ b/scripts/build-platforms-generate.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash + +set -o errexit # abort on nonzero exitstatus +set -o nounset # abort on unbound variable +set -o pipefail # don't hide errors within pipes + +shopt -s globstar +shopt -s nullglob + +# Using shards to optimise tests +# In the future we can incorporate test durations rather than using +# a static value for the parallel keyword + +# Number of parallel shards to split the test suite into +CI_PARALLEL=2 + +# Quote the heredoc to prevent shell expansion +cat << "EOF" +variables: + GIT_SUBMODULE_STRATEGY: "recursive" + GH_PROJECT_PATH: "MatrixAI/${CI_PROJECT_NAME}" + GH_PROJECT_URL: "https://${GITHUB_TOKEN}@github.com/${GH_PROJECT_PATH}.git" + # Cache .npm + npm_config_cache: "${CI_PROJECT_DIR}/tmp/npm" + # Prefer offline node module installation + npm_config_prefer_offline: "true" + # Homebrew cache only used by macos runner + HOMEBREW_CACHE: "${CI_PROJECT_DIR}/tmp/Homebrew" + +default: + interruptible: true + before_script: + # Replace this in windows runners that use powershell + # with `mkdir -Force "$CI_PROJECT_DIR/tmp"` + - mkdir -p "$CI_PROJECT_DIR/tmp" + +# Cached directories shared between jobs & pipelines per-branch per-runner +cache: + key: $CI_COMMIT_REF_SLUG + # Preserve cache even if job fails + when: 'always' + paths: + - ./tmp/npm/ + # Homebrew cache is only used by the macos runner + - ./tmp/Homebrew + # Chocolatey cache is only used by the windows runner + - ./tmp/chocolatey/ + # `jest` cache is configured in jest.config.js + - ./tmp/jest/ + +stages: + - build # Cross-platform library compilation, unit tests + +image: registry.gitlab.com/matrixai/engineering/maintenance/gitlab-runner + +build:linux: + stage: build + needs: [] +EOF +cat << EOF + parallel: $CI_PARALLEL +EOF +cat << "EOF" + script: + - > + nix-shell --arg ci true --run $' + npm test -- --ci --coverage --shard="$CI_NODE_INDEX/$CI_NODE_TOTAL"; + ' + artifacts: + when: always + reports: + junit: + - ./tmp/junit/junit.xml + coverage_report: + coverage_format: cobertura + path: ./tmp/coverage/cobertura-coverage.xml + coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' + +build:windows: + stage: build + needs: [] +EOF +cat << EOF + parallel: $CI_PARALLEL +EOF +cat << "EOF" + tags: + - windows + before_script: + - mkdir -Force "$CI_PROJECT_DIR/tmp" + script: + - .\scripts\choco-install.ps1 + - refreshenv + - npm install --ignore-scripts + - $env:Path = "$(npm root)\.bin;" + $env:Path + - npm test -- --ci --coverage --shard="$CI_NODE_INDEX/$CI_NODE_TOTAL" + artifacts: + when: always + reports: + junit: + - ./tmp/junit/junit.xml + coverage_report: + coverage_format: cobertura + path: ./tmp/coverage/cobertura-coverage.xml + coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' + +build:macos: + stage: build + needs: [] +EOF +cat << EOF + parallel: $CI_PARALLEL +EOF +cat << "EOF" + tags: + - saas-macos-medium-m1 + image: macos-12-xcode-14 + script: + - eval "$(brew shellenv)" + - ./scripts/brew-install.sh + - hash -r + - npm install --ignore-scripts + - export PATH="$(npm root)/.bin:$PATH" + - npm test -- --ci --coverage --shard="$CI_NODE_INDEX/$CI_NODE_TOTAL" + artifacts: + when: always + reports: + junit: + - ./tmp/junit/junit.xml + coverage_report: + coverage_format: cobertura + path: ./tmp/coverage/cobertura-coverage.xml + coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' +EOF diff --git a/scripts/check-test-generate.sh b/scripts/check-test-generate.sh new file mode 100755 index 00000000..66f3b529 --- /dev/null +++ b/scripts/check-test-generate.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -o errexit # abort on nonzero exitstatus +set -o nounset # abort on unbound variable +set -o pipefail # don't hide errors within pipes + +shopt -s globstar +shopt -s nullglob + +# Using shards to optimise tests +# In the future we can incorporate test durations rather than using +# a static value for the parallel keyword + +# Number of parallel shards to split the test suite into +CI_PARALLEL=2 + +# Quote the heredoc to prevent shell expansion +cat << "EOF" +variables: + GIT_SUBMODULE_STRATEGY: "recursive" + GH_PROJECT_PATH: "MatrixAI/${CI_PROJECT_NAME}" + GH_PROJECT_URL: "https://${GITHUB_TOKEN}@github.com/${GH_PROJECT_PATH}.git" + # Cache .npm + npm_config_cache: "${CI_PROJECT_DIR}/tmp/npm" + # Prefer offline node module installation + npm_config_prefer_offline: "true" + # Homebrew cache only used by macos runner + HOMEBREW_CACHE: "${CI_PROJECT_DIR}/tmp/Homebrew" + +default: + interruptible: true + before_script: + # Replace this in windows runners that use powershell + # with `mkdir -Force "$CI_PROJECT_DIR/tmp"` + - mkdir -p "$CI_PROJECT_DIR/tmp" + +# Cached directories shared between jobs & pipelines per-branch per-runner +cache: + key: $CI_COMMIT_REF_SLUG + # Preserve cache even if job fails + when: 'always' + paths: + - ./tmp/npm/ + # Homebrew cache is only used by the macos runner + - ./tmp/Homebrew + # Chocolatey cache is only used by the windows runner + - ./tmp/chocolatey/ + # `jest` cache is configured in jest.config.js + - ./tmp/jest/ + +stages: + - check # Linting, unit tests + +image: registry.gitlab.com/matrixai/engineering/maintenance/gitlab-runner + +check:test: + stage: check + needs: [] +EOF +cat << EOF + parallel: $CI_PARALLEL +EOF +cat << "EOF" + script: + - > + nix-shell --arg ci true --run $' + npm test -- --ci --coverage --shard="$CI_NODE_INDEX/$CI_NODE_TOTAL"; + ' + artifacts: + when: always + reports: + junit: + - ./tmp/junit/junit.xml + coverage_report: + coverage_format: cobertura + path: ./tmp/coverage/cobertura-coverage.xml + coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' +EOF diff --git a/scripts/choco-install.ps1 b/scripts/choco-install.ps1 new file mode 100755 index 00000000..815038bf --- /dev/null +++ b/scripts/choco-install.ps1 @@ -0,0 +1,32 @@ +$ErrorActionPreference = "Stop" + +function Save-ChocoPackage { + param ( + $PackageName + ) + Rename-Item -Path "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nupkg" -NewName "$PackageName.nupkg.zip" -ErrorAction:SilentlyContinue + Expand-Archive -LiteralPath "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nupkg.zip" -DestinationPath "$env:ChocolateyInstall\lib\$PackageName" -Force + Remove-Item "$env:ChocolateyInstall\lib\$PackageName\_rels" -Recurse + Remove-Item "$env:ChocolateyInstall\lib\$PackageName\package" -Recurse + Remove-Item "$env:ChocolateyInstall\lib\$PackageName\[Content_Types].xml" + New-Item -Path "${PSScriptRoot}\..\tmp\chocolatey\$PackageName" -ItemType "directory" -ErrorAction:SilentlyContinue + choco pack "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nuspec" --outdir "${PSScriptRoot}\..\tmp\chocolatey\$PackageName" +} + +# Check for existence of required environment variables +if ( $null -eq $env:ChocolateyInstall ) { + [Console]::Error.WriteLine('Missing $env:ChocolateyInstall environment variable') + exit 1 +} + +# Add the cached packages with source priority 1 (Chocolatey community is 0) +New-Item -Path "${PSScriptRoot}\..\tmp\chocolatey" -ItemType "directory" -ErrorAction:SilentlyContinue +choco source add --name="cache" --source="${PSScriptRoot}\..\tmp\chocolatey" --priority=1 + +# Install nodejs v18.15.0 (will use cache if exists) +$nodejs = "nodejs.install" +choco install "$nodejs" --version="18.15.0" --require-checksums -y +# Internalise nodejs to cache if doesn't exist +if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$nodejs\$nodejs.18.15.0.nupkg" -PathType Leaf) ) { + Save-ChocoPackage -PackageName $nodejs +} diff --git a/scripts/deploy-image.sh b/scripts/deploy-image.sh new file mode 100755 index 00000000..7806cf05 --- /dev/null +++ b/scripts/deploy-image.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -o errexit # abort on nonzero exitstatus +set -o nounset # abort on unbound variable +set -o pipefail # don't hide errors within pipes + +image="$1" +tag="$2" +registry_image="$3" + +if [ -z "$image" ]; then + printf '%s\n' 'Unset or empty path to container image archive' >&2 + exit 1 +fi + +if [ -z "$tag" ]; then + printf '%s\n' 'Unset or empty custom tag for target registry' >&2 + exit 1 +fi + +if [ -z "$registry_image" ]; then + printf '%s\n' 'Unset or empty image registry path' >&2 + exit 1 +fi + +default_tag="$(skopeo --tmpdir "${TMPDIR-/tmp}" list-tags "docker-archive:$image" \ + | jq -r '.Tags[0] | split(":")[1]')" + +skopeo \ + --insecure-policy \ + --tmpdir "${TMPDIR-/tmp}" \ + copy \ + "docker-archive:$image" \ + "docker://$registry_image:$default_tag" + +# Cannot use `--additional-tag` for ECR +# each tag must be additionally set + +skopeo \ + --insecure-policy \ + --tmpdir "${TMPDIR-/tmp}" \ + copy \ + "docker://$registry_image:$default_tag" \ + "docker://$registry_image:$tag" & + +skopeo \ + --insecure-policy \ + --tmpdir "${TMPDIR-/tmp}" \ + copy \ + "docker://$registry_image:$default_tag" \ + "docker://$registry_image:latest" & + +wait diff --git a/scripts/pkg.js b/scripts/pkg.js new file mode 100755 index 00000000..4a49a390 --- /dev/null +++ b/scripts/pkg.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +const os = require('os'); +const fs = require('fs'); +const path = require('path'); +const process = require('process'); +const childProcess = require('child_process'); +const packageJSON = require('../package.json'); + +/** + * Supported platforms + * Maps os.platform() to pkg platform + */ +const platforms = { + linux: 'linux', + win32: 'win', + darwin: 'macos', +}; + +/** + * Supported architectures + * Maps os.arch() to pkg arch + */ +const archs = { + x64: 'x64', + arm64: 'arm64', +}; + +async function find(dirPath, pattern) { + const found = []; + let entries; + try { + entries = await fs.promises.readdir(dirPath); + } catch (e) { + if (e.code === 'ENOENT') { + return found; + } + throw e; + } + for (const entry of entries) { + const entryPath = path.join(dirPath, entry); + const stat = await fs.promises.lstat(entryPath); + if (stat.isDirectory()) { + found.push(...(await find(entryPath, pattern))); + } else if (pattern.test(entryPath)) { + found.push(entryPath); + } + } + return found; +} + +/* eslint-disable no-console */ +async function main(argv = process.argv) { + argv = argv.slice(2); + let outPath; + let binTarget; + let nodeVersion = process.versions.node.match(/\d+/)[0]; + let platform = os.platform(); + let arch = os.arch(); + const restArgs = []; + while (argv.length > 0) { + const option = argv.shift(); + let match; + if ((match = option.match(/--output(?:=(.+)|$)/))) { + outPath = match[1] ?? argv.shift(); + } else if ((match = option.match(/--bin(?:=(.+)|$)/))) { + binTarget = match[1] ?? argv.shift(); + } else if ((match = option.match(/--node-version(?:=(.+)|$)/))) { + nodeVersion = match[1] ?? argv.shift(); + } else if ((match = option.match(/--platform(?:=(.+)|$)/))) { + platform = match[1] ?? argv.shift(); + } else if ((match = option.match(/--arch(?:=(.+)|$)/))) { + arch = match[1] ?? argv.shift(); + } else { + restArgs.push(option); + } + } + let entryPoint; + if (binTarget == null) { + entryPoint = Object.values(packageJSON.bin ?? {})[0]; + } else { + entryPoint = packageJSON.bin[binTarget]; + } + if (entryPoint == null) { + throw new Error('Bin executable is required'); + } + if (typeof outPath !== 'string') { + throw new Error('Output path is required'); + } + if (entryPoint == null) { + throw new Error(`Unknown bin target: ${binTarget}`); + } + if (isNaN(parseInt(nodeVersion))) { + throw new Error(`Unsupported node version: ${nodeVersion}`); + } + if (!(platform in platforms)) { + throw new Error(`Unsupported platform: ${platform}`); + } + if (!(arch in archs)) { + throw new Error(`Unsupported architecture: ${arch}`); + } + // Monkey patch the os.platform and os.arch for node-gyp-build + os.platform = () => platform; + os.arch = () => arch; + // Ensure that `node-gyp-build` only finds prebuilds + process.env.PREBUILDS_ONLY = '1'; + const nodeGypBuild = require('node-gyp-build'); + const pkgConfig = packageJSON.pkg ?? {}; + pkgConfig.assets = pkgConfig.assets ?? {}; + const npmLsOut = childProcess.execFileSync( + 'npm', + ['ls', '--all', '--prod', '--parseable'], + { + windowsHide: true, + encoding: 'utf-8', + }, + ); + const nodePackages = npmLsOut.trim().split('\n'); + const projectRoot = path.join(__dirname, '..'); + for (const nodePackage of nodePackages) { + // If `build` or `prebuilds` directory exists with a `.node` file + // then we expect to find the appropriate native addon + // The `node-gyp-build` will look in these 2 directories + const buildPath = path.join(nodePackage, 'build'); + const prebuildsPath = path.join(nodePackage, 'prebuilds'); + const buildFinds = await find(buildPath, /.node$/); + const prebuildsFinds = await find(prebuildsPath, /.node$/); + if (buildFinds.length > 0 || prebuildsFinds.length > 0) { + let nativeAddonPath = nodeGypBuild.path(nodePackage); + // Must use relative paths + // so that assets are added relative to the project + nativeAddonPath = path.relative(projectRoot, nativeAddonPath); + pkgConfig.assets.push(nativeAddonPath); + } + } + console.error('Configured pkg with:'); + console.error(pkgConfig); + // The pkg config must be in the same directory as the `package.json` + // otherwise the relative paths won't work + const pkgConfigPath = path.join(projectRoot, 'pkg.json'); + await fs.promises.writeFile(pkgConfigPath, JSON.stringify(pkgConfig)); + const pkgPlatform = platforms[platform]; + const pkgArch = archs[arch]; + const pkgArgs = [ + entryPoint, + `--config=${pkgConfigPath}`, + `--targets=node${nodeVersion}-${pkgPlatform}-${pkgArch}`, + '--no-bytecode', + '--no-native-build', + '--public', + "--public-packages='*'", + `--output=${outPath}`, + ...restArgs, + ]; + console.error('Running pkg:'); + console.error(['pkg', ...pkgArgs].join(' ')); + childProcess.execFileSync('pkg', pkgArgs, { + stdio: ['inherit', 'inherit', 'inherit'], + windowsHide: true, + encoding: 'utf-8', + }); + await fs.promises.rm(pkgConfigPath); +} +/* eslint-enable no-console */ + +void main(); diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..9302401c --- /dev/null +++ b/shell.nix @@ -0,0 +1,41 @@ +{ pkgs ? import ./pkgs.nix {}, ci ? false }: + +with pkgs; +let + utils = callPackage ./utils.nix {}; +in + mkShell { + nativeBuildInputs = [ + nodejs + shellcheck + gitAndTools.gh + skopeo + jq + ]; + PKG_CACHE_PATH = utils.pkgCachePath; + PKG_IGNORE_TAG = 1; + shellHook = '' + echo "Entering $(npm pkg get name)" + set -o allexport + . ./.env + set +o allexport + set -v + ${ + lib.optionalString ci + '' + set -o errexit + set -o nounset + set -o pipefail + shopt -s inherit_errexit + '' + } + mkdir --parents "$(pwd)/tmp" + + # Built executables and NPM executables + export PATH="$(pwd)/dist/bin:$(npm root)/.bin:$PATH" + + npm install --ignore-scripts + + set +v + ''; + } diff --git a/src/.eslintrc b/src/.eslintrc new file mode 100644 index 00000000..22f32604 --- /dev/null +++ b/src/.eslintrc @@ -0,0 +1,21 @@ +{ + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "@", + "message": "Replace with relative path" + } + ], + "patterns": [ + { + "group": ["@/**"], + "message": "Replace with relative path" + } + ] + } + ] + } +} diff --git a/src/CommandPolykey.ts b/src/CommandPolykey.ts new file mode 100644 index 00000000..a92f88ca --- /dev/null +++ b/src/CommandPolykey.ts @@ -0,0 +1,101 @@ +import type { FileSystem } from 'polykey/dist/types'; +import commander from 'commander'; +import Logger, { + StreamHandler, + formatting, + levelToString, + evalLogDataValue, +} from '@matrixai/logger'; +import * as binUtils from './utils'; +import * as binOptions from './utils/options'; +import * as binErrors from './errors'; + +/** + * Singleton logger constructed once for all commands + */ +const logger = new Logger('polykey', undefined, [new StreamHandler()]); + +/** + * Base class for all commands + */ +class CommandPolykey extends commander.Command { + protected logger: Logger = logger; + protected fs: FileSystem; + protected exitHandlers: binUtils.ExitHandlers; + + public constructor({ + exitHandlers, + fs = require('fs'), + }: { + exitHandlers: binUtils.ExitHandlers; + fs?: FileSystem; + }) { + super(); + this.fs = fs; + this.exitHandlers = exitHandlers; + // All commands must not exit upon error + this.exitOverride(); + // On usage error, show the help info + this.showHelpAfterError(); + // On usage error, auto-suggest alternatives + this.showSuggestionAfterError(); + // Add all default options + // these options will be available across the command hierarchy + // the values will be captured by the root command + this.addOption(binOptions.nodePath); + this.addOption(binOptions.passwordFile); + this.addOption(binOptions.format); + this.addOption(binOptions.verbose); + } + + /** + * Overrides opts to return all options set in the command hierarchy + */ + public opts(): T { + const opts = super.opts(); + if (this.parent != null) { + // Override the current options with parent options + // global option values are captured by the root command + return Object.assign(opts, this.parent.opts()); + } else { + return opts; + } + } + + public action(fn: (...args: any[]) => void | Promise): this { + return super.action(async (...args: any[]) => { + const opts = this.opts(); + // Set the format for error logging for the exit handlers + this.exitHandlers.errFormat = opts.format === 'json' ? 'json' : 'error'; + // Set the logger according to the verbosity + this.logger.setLevel(binUtils.verboseToLogLevel(opts.verbose)); + // Set the logger formatter according to the format + if (opts.format === 'json') { + this.logger.handlers.forEach((handler) => + handler.setFormatter((record) => { + return JSON.stringify( + { + level: levelToString(record.level), + keys: record.keys, + msg: record.msg, + ...record.data, + }, + evalLogDataValue, + ); + }), + ); + } else { + const format = formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`; + this.logger.handlers.forEach((handler) => handler.setFormatter(format)); + } + // If the node path is undefined + // this means there is an unknown platform + if (opts.nodePath == null) { + throw new binErrors.ErrorCLINodePath(); + } + await fn(...args); + }); + } +} + +export default CommandPolykey; diff --git a/src/agent/CommandAgent.ts b/src/agent/CommandAgent.ts new file mode 100644 index 00000000..8e83ac63 --- /dev/null +++ b/src/agent/CommandAgent.ts @@ -0,0 +1,23 @@ +import CommandLock from './CommandLock'; +import CommandLockAll from './CommandLockAll'; +import CommandStart from './CommandStart'; +import CommandStatus from './CommandStatus'; +import CommandStop from './CommandStop'; +import CommandUnlock from './CommandUnlock'; +import CommandPolykey from '../CommandPolykey'; + +class CommandAgent extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('agent'); + this.description('Agent Operations'); + this.addCommand(new CommandLock(...args)); + this.addCommand(new CommandLockAll(...args)); + this.addCommand(new CommandStart(...args)); + this.addCommand(new CommandStatus(...args)); + this.addCommand(new CommandStop(...args)); + this.addCommand(new CommandUnlock(...args)); + } +} + +export default CommandAgent; diff --git a/src/agent/CommandLock.ts b/src/agent/CommandLock.ts new file mode 100644 index 00000000..f48553f2 --- /dev/null +++ b/src/agent/CommandLock.ts @@ -0,0 +1,28 @@ +import path from 'path'; +import config from 'polykey/dist/config'; +import CommandPolykey from '../CommandPolykey'; + +class CommandLock extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('lock'); + this.description('Lock the Client and Clear the Existing Token'); + this.action(async (options) => { + const { default: Session } = await import( + 'polykey/dist/sessions/Session' + ); + const session = new Session({ + sessionTokenPath: path.join( + options.nodePath, + config.defaults.tokenBase, + ), + fs: this.fs, + logger: this.logger.getChild(Session.name), + }); + // Destroy local session + await session.destroy(); + }); + } +} + +export default CommandLock; diff --git a/src/agent/CommandLockAll.ts b/src/agent/CommandLockAll.ts new file mode 100644 index 00000000..6b748327 --- /dev/null +++ b/src/agent/CommandLockAll.ts @@ -0,0 +1,83 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import path from 'path'; +import config from 'polykey/dist/config'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandLockAll extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('lockall'); + this.description('Lock all Clients and Clear the Existing Token'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const { default: Session } = await import( + 'polykey/dist/sessions/Session' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const session = new Session({ + sessionTokenPath: path.join( + options.nodePath, + config.defaults.tokenBase, + ), + fs: this.fs, + logger: this.logger.getChild(Session.name), + }); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.agentLockAll({ + metadata: auth, + }), + auth, + ); + // Destroy local session + await session.destroy(); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandLockAll; diff --git a/src/agent/CommandStart.ts b/src/agent/CommandStart.ts new file mode 100644 index 00000000..db4341da --- /dev/null +++ b/src/agent/CommandStart.ts @@ -0,0 +1,250 @@ +import type { StdioOptions } from 'child_process'; +import type { + AgentStatusLiveData, + AgentChildProcessInput, + AgentChildProcessOutput, +} from '../types'; +import type PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import type { RecoveryCode } from 'polykey/dist/keys/types'; +import path from 'path'; +import childProcess from 'child_process'; +import process from 'process'; +import * as keysErrors from 'polykey/dist/keys/errors'; +import { promise, dirEmpty } from 'polykey/dist/utils'; +import config from 'polykey/dist/config'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binErrors from '../errors'; + +class CommandStart extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('start'); + this.description('Start the Polykey Agent'); + this.addOption(binOptions.recoveryCodeFile); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.addOption(binOptions.agentHost); + this.addOption(binOptions.agentPort); + this.addOption(binOptions.connConnectTime); + this.addOption(binOptions.seedNodes); + this.addOption(binOptions.network); + this.addOption(binOptions.workers); + this.addOption(binOptions.background); + this.addOption(binOptions.backgroundOutFile); + this.addOption(binOptions.backgroundErrFile); + this.addOption(binOptions.fresh); + this.addOption(binOptions.privateKeyFile); + this.addOption(binOptions.passwordOpsLimit); + this.addOption(binOptions.passwordMemLimit); + this.action(async (options) => { + options.clientHost = + options.clientHost ?? config.defaults.networkConfig.clientHost; + options.clientPort = + options.clientPort ?? config.defaults.networkConfig.clientPort; + const { default: PolykeyAgent } = await import( + 'polykey/dist/PolykeyAgent' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const keysUtils = await import('polykey/dist/keys/utils'); + let password: string | undefined; + if (options.fresh) { + // If fresh, then get a new password + password = await binProcessors.processNewPassword( + options.passwordFile, + this.fs, + ); + } else if (options.recoveryCodeFile != null) { + // If recovery code is supplied, then this is the new password + password = await binProcessors.processNewPassword( + options.passwordFile, + this.fs, + ); + } else if (await dirEmpty(this.fs, options.nodePath)) { + // If the node path is empty, get a new password + password = await binProcessors.processNewPassword( + options.passwordFile, + this.fs, + ); + } else { + // Otherwise this is the existing password + // however, the code is capable of doing partial bootstrapping, + // so it's possible that this is also a new password + // if the root key isn't setup + password = await binProcessors.processPassword( + options.passwordFile, + this.fs, + ); + } + const recoveryCodeIn = await binProcessors.processRecoveryCode( + options.recoveryCodeFile, + this.fs, + ); + // Will be `[{}, true]` if `--seed-nodes` is not set + // Will be '[{}, true]' if `--seed-nodes=''` + // Will be '[{...}, true]' if `--seed-nodes='...;'` + // Will be '[{}, false]' if `--seed-nodes=''` + // Will be '[{...}, false]' if `--seed-nodes='...'` + const [seedNodes, defaults] = options.seedNodes; + let seedNodes_ = seedNodes; + if (defaults) seedNodes_ = { ...options.network, ...seedNodes }; + const agentConfig = { + password, + nodePath: options.nodePath, + keyRingConfig: { + recoveryCode: recoveryCodeIn, + privateKeyPath: options.privateKeyFile, + passwordOpsLimit: + keysUtils.passwordOpsLimits[options.passwordOpsLimit], + passwordMemLimit: + keysUtils.passwordMemLimits[options.passwordMemLimit], + }, + networkConfig: { + clientHost: options.clientHost, + clientPort: options.clientPort, + agentHost: options.agentHost, + agentPort: options.agentPort, + }, + seedNodes: seedNodes_, + workers: options.workers, + fresh: options.fresh, + }; + let statusLiveData: AgentStatusLiveData; + let recoveryCodeOut: RecoveryCode | undefined; + if (options.background) { + const stdio: StdioOptions = ['ignore', 'ignore', 'ignore', 'ipc']; + if (options.backgroundOutFile != null) { + const agentOutFile = await this.fs.promises.open( + options.backgroundOutFile, + 'w', + ); + stdio[1] = agentOutFile.fd; + } + if (options.backgroundErrFile != null) { + const agentErrFile = await this.fs.promises.open( + options.backgroundErrFile, + 'w', + ); + stdio[2] = agentErrFile.fd; + } + const agentProcess = childProcess.fork( + path.join(__dirname, '../polykey-agent'), + [], + { + cwd: process.cwd(), + env: process.env, + detached: true, + serialization: 'advanced', + stdio, + }, + ); + const { + p: agentProcessP, + resolveP: resolveAgentProcessP, + rejectP: rejectAgentProcessP, + } = promise(); + // Once the agent responds with message, it considered ok to go-ahead + agentProcess.once('message', (messageOut: AgentChildProcessOutput) => { + if (messageOut.status === 'SUCCESS') { + agentProcess.unref(); + agentProcess.disconnect(); + recoveryCodeOut = messageOut.recoveryCode; + statusLiveData = { ...messageOut }; + delete statusLiveData['recoveryCode']; + delete statusLiveData['status']; + resolveAgentProcessP(); + return; + } else { + rejectAgentProcessP( + new binErrors.ErrorCLIPolykeyAgentProcess( + 'Agent process responded with error', + messageOut.error, + ), + ); + return; + } + }); + // Handle error event during abnormal spawning, this is rare + agentProcess.once('error', (e) => { + rejectAgentProcessP( + new binErrors.ErrorCLIPolykeyAgentProcess(e.message), + ); + }); + // If the process exits during initial execution of polykey-agent script + // Then it is an exception, because the agent process is meant to be a long-running daemon + agentProcess.once('close', (code, signal) => { + rejectAgentProcessP( + new binErrors.ErrorCLIPolykeyAgentProcess( + 'Agent process closed during fork', + { + data: { + code, + signal, + }, + }, + ), + ); + }); + const messageIn: AgentChildProcessInput = { + logLevel: this.logger.getEffectiveLevel(), + format: options.format, + agentConfig, + }; + agentProcess.send(messageIn, (e) => { + if (e != null) { + rejectAgentProcessP( + new binErrors.ErrorCLIPolykeyAgentProcess( + 'Failed sending agent process message', + ), + ); + } + }); + await agentProcessP; + } else { + // Change process name to polykey-agent + process.title = 'polykey-agent'; + // eslint-disable-next-line prefer-const + let pkAgent: PolykeyAgent; + this.exitHandlers.handlers.push(async () => { + await pkAgent?.stop(); + }); + try { + pkAgent = await PolykeyAgent.createPolykeyAgent({ + fs: this.fs, + logger: this.logger.getChild(PolykeyAgent.name), + ...agentConfig, + }); + } catch (e) { + if (e instanceof keysErrors.ErrorKeyPairParse) { + throw new binErrors.ErrorCLIPasswordWrong(); + } + throw e; + } + recoveryCodeOut = pkAgent.keyRing.recoveryCode; + statusLiveData = { + pid: process.pid, + nodeId: nodesUtils.encodeNodeId(pkAgent.keyRing.getNodeId()), + clientHost: pkAgent.webSocketServerClient.getHost(), + clientPort: pkAgent.webSocketServerClient.getPort(), + agentHost: pkAgent.quicServerAgent.host, + agentPort: pkAgent.quicServerAgent.port, + }; + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: { + ...statusLiveData!, + ...(recoveryCodeOut != null + ? { recoveryCode: recoveryCodeOut } + : {}), + }, + }), + ); + }); + } +} + +export default CommandStart; diff --git a/src/agent/CommandStatus.ts b/src/agent/CommandStatus.ts new file mode 100644 index 00000000..ceef06a3 --- /dev/null +++ b/src/agent/CommandStatus.ts @@ -0,0 +1,101 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { StatusResultMessage } from 'polykey/dist/client/handlers/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandStatus extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('status'); + this.description('Get the Status of the Polykey Agent'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientStatus = await binProcessors.processClientStatus( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const statusInfo = clientStatus.statusInfo; + // If status is not LIVE, we return what we have in the status info + // If status is LIVE, then we connect and acquire agent information + if (statusInfo != null && statusInfo?.status !== 'LIVE') { + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: { + status: statusInfo.status, + ...statusInfo.data, + }, + }), + ); + } else { + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + let response: StatusResultMessage; + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientStatus.nodeId!], + host: clientStatus.clientHost!, + port: clientStatus.clientPort!, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.agentStatus({ + metadata: auth, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: { + status: 'LIVE', + pid: response.pid, + nodeId: response.nodeIdEncoded, + clientHost: response.clientHost, + clientPort: response.clientPort, + agentHost: response.agentHost, + agentPort: response.agentPort, + publicKeyJWK: response.publicKeyJwk, + certChainPEM: response.certChainPEM, + }, + }), + ); + } + }); + } +} + +export default CommandStatus; diff --git a/src/agent/CommandStop.ts b/src/agent/CommandStop.ts new file mode 100644 index 00000000..92738e80 --- /dev/null +++ b/src/agent/CommandStop.ts @@ -0,0 +1,82 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binErrors from '../errors'; + +class CommandStop extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('stop'); + this.description('Stop the Polykey Agent'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientStatus = await binProcessors.processClientStatus( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const statusInfo = clientStatus.statusInfo; + if (statusInfo?.status === 'DEAD') { + this.logger.info('Agent is already dead'); + return; + } else if (statusInfo?.status === 'STOPPING') { + this.logger.info('Agent is already stopping'); + return; + } else if (statusInfo?.status === 'STARTING') { + throw new binErrors.ErrorCLIPolykeyAgentStatus('agent is starting'); + } + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + // Either the statusInfo is undefined or LIVE + // Either way, the connection parameters now exist + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientStatus.nodeId!], + host: clientStatus.clientHost!, + port: clientStatus.clientPort!, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.agentStop({ + metadata: auth, + }), + auth, + ); + this.logger.info('Stopping Agent'); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandStop; diff --git a/src/agent/CommandUnlock.ts b/src/agent/CommandUnlock.ts new file mode 100644 index 00000000..eb5f58a6 --- /dev/null +++ b/src/agent/CommandUnlock.ts @@ -0,0 +1,68 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandUnlock extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('unlock'); + this.description('Request a New Token and Start a New Session'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.agentUnlock({ + metadata: auth, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandUnlock; diff --git a/src/agent/index.ts b/src/agent/index.ts new file mode 100644 index 00000000..bb8b521a --- /dev/null +++ b/src/agent/index.ts @@ -0,0 +1 @@ +export { default } from './CommandAgent'; diff --git a/src/bootstrap/CommandBootstrap.ts b/src/bootstrap/CommandBootstrap.ts new file mode 100644 index 00000000..f55ff7af --- /dev/null +++ b/src/bootstrap/CommandBootstrap.ts @@ -0,0 +1,48 @@ +import process from 'process'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandBootstrap extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('bootstrap'); + this.description('Bootstrap Keynode State'); + this.addOption(binOptions.recoveryCodeFile); + this.addOption(binOptions.fresh); + this.addOption(binOptions.privateKeyFile); + this.addOption(binOptions.passwordOpsLimit); + this.addOption(binOptions.passwordMemLimit); + this.action(async (options) => { + const bootstrapUtils = await import('polykey/dist/bootstrap/utils'); + const keysUtils = await import('polykey/dist/keys/utils'); + const password = await binProcessors.processNewPassword( + options.passwordFile, + this.fs, + ); + const recoveryCodeIn = await binProcessors.processRecoveryCode( + options.recoveryCodeFile, + this.fs, + ); + const recoveryCodeOut = await bootstrapUtils.bootstrapState({ + password, + nodePath: options.nodePath, + keyRingConfig: { + recoveryCode: recoveryCodeIn, + privateKeyPath: options.privateKeyFile, + passwordOpsLimit: + keysUtils.passwordOpsLimits[options.passwordOpsLimit], + passwordMemLimit: + keysUtils.passwordMemLimits[options.passwordMemLimit], + }, + fresh: options.fresh, + fs: this.fs, + logger: this.logger, + }); + this.logger.info(`Bootstrapped ${options.nodePath}`); + if (recoveryCodeOut != null) process.stdout.write(recoveryCodeOut + '\n'); + }); + } +} + +export default CommandBootstrap; diff --git a/src/bootstrap/index.ts b/src/bootstrap/index.ts new file mode 100644 index 00000000..30c5ebcf --- /dev/null +++ b/src/bootstrap/index.ts @@ -0,0 +1 @@ +export { default } from './CommandBootstrap'; diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 00000000..17f20989 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,109 @@ +import ErrorPolykey from 'polykey/dist/ErrorPolykey'; +import sysexits from 'polykey/dist/utils/sysexits'; + +class ErrorBin extends ErrorPolykey {} + +class ErrorBinUncaughtException extends ErrorBin { + static description = ''; + exitCode = sysexits.SOFTWARE; +} + +class ErrorBinUnhandledRejection extends ErrorBin { + static description = ''; + exitCode = sysexits.SOFTWARE; +} + +class ErrorBinAsynchronousDeadlock extends ErrorBin { + static description = + 'PolykeyAgent process exited unexpectedly, likely due to promise deadlock'; + exitCode = sysexits.SOFTWARE; +} + +class ErrorCLI extends ErrorBin {} + +class ErrorCLINodePath extends ErrorCLI { + static description = 'Cannot derive default node path from unknown platform'; + exitCode = sysexits.USAGE; +} + +class ErrorCLIClientOptions extends ErrorCLI { + static description = 'Missing required client options'; + exitCode = sysexits.USAGE; +} + +class ErrorCLIPasswordWrong extends ErrorCLI { + static description = 'Wrong password, please try again'; + exitCode = sysexits.USAGE; +} + +class ErrorCLIPasswordMissing extends ErrorCLI { + static description = + 'Password is necessary, provide it via --password-file, PK_PASSWORD or when prompted'; + exitCode = sysexits.USAGE; +} + +class ErrorCLIPasswordFileRead extends ErrorCLI { + static description = 'Failed to read password file'; + exitCode = sysexits.NOINPUT; +} + +class ErrorCLIRecoveryCodeFileRead extends ErrorCLI { + static description = 'Failed to read recovery code file'; + exitCode = sysexits.NOINPUT; +} + +class ErrorCLIPrivateKeyFileRead extends ErrorCLI { + static description = 'Failed to read private key Pem file'; + exitCode = sysexits.NOINPUT; +} + +class ErrorCLIPublicJWKFileRead extends ErrorCLI { + static description = 'Failed to read public JWK file'; + exitCode = sysexits.NOINPUT; +} + +class ErrorCLIFileRead extends ErrorCLI { + static description = 'Failed to read file'; + exitCode = sysexits.NOINPUT; +} + +class ErrorCLIPolykeyAgentStatus extends ErrorCLI { + static description = 'PolykeyAgent agent status'; + exitCode = sysexits.TEMPFAIL; +} + +class ErrorCLIPolykeyAgentProcess extends ErrorCLI { + static description = 'PolykeyAgent process could not be started'; + exitCode = sysexits.OSERR; +} + +class ErrorCLINodeFindFailed extends ErrorCLI { + static description = 'Failed to find the node in the DHT'; + exitCode = 1; +} + +class ErrorCLINodePingFailed extends ErrorCLI { + static description = 'Node was not online or not found.'; + exitCode = 1; +} + +export { + ErrorBin, + ErrorBinUncaughtException, + ErrorBinUnhandledRejection, + ErrorBinAsynchronousDeadlock, + ErrorCLI, + ErrorCLINodePath, + ErrorCLIClientOptions, + ErrorCLIPasswordWrong, + ErrorCLIPasswordMissing, + ErrorCLIPasswordFileRead, + ErrorCLIRecoveryCodeFileRead, + ErrorCLIPrivateKeyFileRead, + ErrorCLIPublicJWKFileRead, + ErrorCLIFileRead, + ErrorCLIPolykeyAgentStatus, + ErrorCLIPolykeyAgentProcess, + ErrorCLINodeFindFailed, + ErrorCLINodePingFailed, +}; diff --git a/src/identities/CommandAllow.ts b/src/identities/CommandAllow.ts new file mode 100644 index 00000000..90d985ca --- /dev/null +++ b/src/identities/CommandAllow.ts @@ -0,0 +1,111 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { GestaltId } from 'polykey/dist/gestalts/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binParsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandAllow extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('allow'); + this.description('Allow Permission for Identity'); + this.argument( + '', + 'Node ID or `Provider ID:Identity ID`', + binParsers.parseGestaltId, + ); + this.argument( + '', + 'Permission to set', + binParsers.parseGestaltAction, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (gestaltId: GestaltId, permission, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const utils = await import('polykey/dist/utils'); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const [type, id] = gestaltId; + switch (type) { + case 'node': + { + // Trusting + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsSetByNode({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(id), + action: permission, + }), + auth, + ); + } + break; + case 'identity': + { + // Setting By Identity + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsSetByIdentity( + { + metadata: auth, + providerId: id[0], + identityId: id[1], + action: permission, + }, + ), + auth, + ); + } + break; + default: + utils.never(); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandAllow; diff --git a/src/identities/CommandAuthenticate.ts b/src/identities/CommandAuthenticate.ts new file mode 100644 index 00000000..9b28c350 --- /dev/null +++ b/src/identities/CommandAuthenticate.ts @@ -0,0 +1,112 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { ClientRPCResponseResult } from 'polykey/dist/client/types'; +import type { AuthProcessMessage } from 'polykey/dist/client/handlers/types'; +import type { ReadableStream } from 'stream/web'; +import * as identitiesUtils from 'polykey/dist/identities/utils'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandAuthenticate extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('authenticate'); + this.description('Authenticate a Digital Identity Provider'); + this.argument( + '', + 'Name of the digital identity provider', + parsers.parseProviderId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (providerId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const { never } = await import('polykey/dist/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let genReadable: ReadableStream< + ClientRPCResponseResult + >; + await binUtils.retryAuthentication(async (auth) => { + genReadable = + await pkClient.rpcClientClient.methods.identitiesAuthenticate({ + metadata: auth, + providerId: providerId, + }); + for await (const message of genReadable) { + if (message.request != null) { + this.logger.info(`Navigate to the URL in order to authenticate`); + this.logger.info( + 'Use any additional additional properties to complete authentication', + ); + identitiesUtils.browser(message.request.url); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: { + url: message.request.url, + ...message.request.dataMap, + }, + }), + ); + } else if (message.response != null) { + this.logger.info( + `Authenticated digital identity provider ${providerId}`, + ); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: [message.response.identityId], + }), + ); + } else { + never(); + } + } + }, auth); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandAuthenticate; diff --git a/src/identities/CommandAuthenticated.ts b/src/identities/CommandAuthenticated.ts new file mode 100644 index 00000000..da3d9557 --- /dev/null +++ b/src/identities/CommandAuthenticated.ts @@ -0,0 +1,86 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandAuthenticated extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('authenticated'); + this.description('Lists all authenticated identities across all providers'); + this.option( + '-pi, --provider-id [providerId]', + 'Digital identity provider to retrieve tokens from', + parsers.parseProviderId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication(async (auth) => { + const readableStream = + await pkClient.rpcClientClient.methods.identitiesAuthenticatedGet({ + metadata: auth, + providerId: options.providerId, + }); + for await (const identityMessage of readableStream) { + const output = { + providerId: identityMessage.providerId, + identityId: identityMessage.identityId, + }; + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: output, + }), + ); + } + }, auth); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandAuthenticated; diff --git a/src/identities/CommandClaim.ts b/src/identities/CommandClaim.ts new file mode 100644 index 00000000..6d815b7e --- /dev/null +++ b/src/identities/CommandClaim.ts @@ -0,0 +1,91 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandClaim extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('claim'); + this.description('Claim a Digital Identity for this Keynode'); + this.argument( + '', + 'Name of the digital identity provider', + parsers.parseProviderId, + ); + this.argument( + '', + 'Digital identity to claim', + parsers.parseIdentityId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (providerId, identityId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const claimMessage = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.identitiesClaim({ + metadata: auth, + providerId: providerId, + identityId: identityId, + }), + auth, + ); + const output = [`Claim Id: ${claimMessage.claimId}`]; + if (claimMessage.url) { + output.push(`Url: ${claimMessage.url}`); + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandClaim; diff --git a/src/identities/CommandDisallow.ts b/src/identities/CommandDisallow.ts new file mode 100644 index 00000000..18696e02 --- /dev/null +++ b/src/identities/CommandDisallow.ts @@ -0,0 +1,111 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { GestaltId } from 'polykey/dist/gestalts/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandDisallow extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('disallow'); + this.description('Disallow Permission for Identity'); + this.argument( + '', + 'Node ID or `Provider Id:Identity Id`', + parsers.parseGestaltId, + ); + this.argument( + '', + 'Permission to unset', + parsers.parseGestaltAction, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (gestaltId: GestaltId, permission, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const utils = await import('polykey/dist/utils'); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const [type, id] = gestaltId; + switch (type) { + case 'node': + { + // Trusting + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsUnsetByNode({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(id), + action: permission, + }), + auth, + ); + } + break; + case 'identity': + { + // Trusting. + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsUnsetByIdentity( + { + metadata: auth, + providerId: id[0], + identityId: id[1], + action: permission, + }, + ), + auth, + ); + } + break; + default: + utils.never(); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandDisallow; diff --git a/src/identities/CommandDiscover.ts b/src/identities/CommandDiscover.ts new file mode 100644 index 00000000..8a8963a4 --- /dev/null +++ b/src/identities/CommandDiscover.ts @@ -0,0 +1,102 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { GestaltId } from 'polykey/dist/gestalts/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandDiscover extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('discover'); + this.description('Adds a Node or Identity to the Discovery Queue'); + this.argument( + '', + 'Node ID or `Provider ID:Identity ID`', + parsers.parseGestaltId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (gestaltId: GestaltId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const utils = await import('polykey/dist/utils'); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const [type, id] = gestaltId; + switch (type) { + case 'node': + { + // Discovery by Node + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsDiscoveryByNode({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(id), + }), + auth, + ); + } + break; + case 'identity': + { + // Discovery by Identity + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsDiscoveryByIdentity({ + metadata: auth, + providerId: id[0], + identityId: id[1], + }), + auth, + ); + } + break; + default: + utils.never(); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandDiscover; diff --git a/src/identities/CommandGet.ts b/src/identities/CommandGet.ts new file mode 100644 index 00000000..08ebef30 --- /dev/null +++ b/src/identities/CommandGet.ts @@ -0,0 +1,130 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { GestaltId } from 'polykey/dist/gestalts/types'; +import type { GestaltMessage } from 'polykey/dist/client/handlers/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandGet extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('get'); + this.description( + 'Gets a Gestalt with a Node or Identity ID from the Gestalt Graph', + ); + this.argument( + '', + 'Node ID or `Provider ID:Identity ID`', + parsers.parseGestaltId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (gestaltId: GestaltId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const utils = await import('polykey/dist/utils'); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let res: GestaltMessage | null = null; + const [type, id] = gestaltId; + switch (type) { + case 'node': + { + // Getting from node + res = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsGestaltGetByNode({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(id), + }), + auth, + ); + } + break; + case 'identity': + { + // Getting from identity. + res = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsGestaltGetByIdentity( + { + metadata: auth, + providerId: id[0], + identityId: id[1], + }, + ), + auth, + ); + } + break; + default: + utils.never(); + } + const gestalt = res!.gestalt; + let output: any = gestalt; + if (options.format !== 'json') { + // Creating a list. + output = []; + // Listing nodes. + for (const nodeKey of Object.keys(gestalt.nodes)) { + const node = gestalt.nodes[nodeKey]; + output.push(`${node.nodeId}`); + } + // Listing identities + for (const identityKey of Object.keys(gestalt.identities)) { + const identity = gestalt.identities[identityKey]; + output.push(`${identity.providerId}:${identity.identityId}`); + } + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandGet; diff --git a/src/identities/CommandIdentities.ts b/src/identities/CommandIdentities.ts new file mode 100644 index 00000000..a629a496 --- /dev/null +++ b/src/identities/CommandIdentities.ts @@ -0,0 +1,37 @@ +import CommandAllow from './CommandAllow'; +import CommandAuthenticate from './CommandAuthenticate'; +import CommandAuthenticated from './CommandAuthenticated'; +import CommandClaim from './CommandClaim'; +import CommandDisallow from './CommandDisallow'; +import CommandDiscover from './CommandDiscover'; +import CommandGet from './CommandGet'; +import CommandList from './CommandList'; +import CommandPermissions from './CommandPermissions'; +import CommandSearch from './CommandSearch'; +import CommandTrust from './CommandTrust'; +import CommandUntrust from './CommandUntrust'; +import CommandInvite from './CommandInvite'; +import CommandPolykey from '../CommandPolykey'; + +class CommandIdentities extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('identities'); + this.description('Identities Operations'); + this.addCommand(new CommandAllow(...args)); + this.addCommand(new CommandAuthenticate(...args)); + this.addCommand(new CommandAuthenticated(...args)); + this.addCommand(new CommandClaim(...args)); + this.addCommand(new CommandDisallow(...args)); + this.addCommand(new CommandDiscover(...args)); + this.addCommand(new CommandGet(...args)); + this.addCommand(new CommandList(...args)); + this.addCommand(new CommandPermissions(...args)); + this.addCommand(new CommandSearch(...args)); + this.addCommand(new CommandTrust(...args)); + this.addCommand(new CommandUntrust(...args)); + this.addCommand(new CommandInvite(...args)); + } +} + +export default CommandIdentities; diff --git a/src/identities/CommandInvite.ts b/src/identities/CommandInvite.ts new file mode 100644 index 00000000..6c199eb9 --- /dev/null +++ b/src/identities/CommandInvite.ts @@ -0,0 +1,87 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; + +class CommandClaim extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('invite'); + this.description('invite another Keynode'); + this.argument( + '', + 'Id of the node to claim', + binParsers.parseNodeId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (nodeId: NodeId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.identitiesInvite({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + }), + auth, + ); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: [ + `Successfully sent Gestalt Invite notification to Keynode with ID ${nodesUtils.encodeNodeId( + nodeId, + )}`, + ], + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandClaim; diff --git a/src/identities/CommandList.ts b/src/identities/CommandList.ts new file mode 100644 index 00000000..1ca19c22 --- /dev/null +++ b/src/identities/CommandList.ts @@ -0,0 +1,128 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as binProcessors from '../utils/processors'; + +class CommandList extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('list'); + this.description('List all the Gestalts in the Gestalt Graph'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let output: any; + const gestalts = await binUtils.retryAuthentication(async (auth) => { + const gestalts: Array = []; + const stream = + await pkClient.rpcClientClient.methods.gestaltsGestaltList({ + metadata: auth, + }); + for await (const gestaltMessage of stream) { + const gestalt = gestaltMessage.gestalt; + const newGestalt: any = { + permissions: [], + nodes: [], + identities: [], + }; + for (const node of Object.keys(gestalt.nodes)) { + const nodeInfo = gestalt.nodes[node]; + newGestalt.nodes.push({ nodeId: nodeInfo.nodeId }); + } + for (const identity of Object.keys(gestalt.identities)) { + const identityInfo = gestalt.identities[identity]; + newGestalt.identities.push({ + providerId: identityInfo.providerId, + identityId: identityInfo.identityId, + }); + } + // Getting the permissions for the gestalt. + const actionsMessage = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsGetByNode({ + metadata: auth, + nodeIdEncoded: newGestalt.nodes[0].nodeId, + }), + auth, + ); + const actionList = actionsMessage.actionsList; + if (actionList.length === 0) newGestalt.permissions = null; + else newGestalt.permissions = actionList; + gestalts.push(newGestalt); + } + return gestalts; + }, auth); + output = gestalts; + if (options.format !== 'json') { + // Convert to a human-readable list. + output = []; + let count = 1; + for (const gestalt of gestalts) { + output.push(`gestalt ${count}`); + output.push(`permissions: ${gestalt.permissions ?? 'None'}`); + // Listing nodes + for (const node of gestalt.nodes) { + output.push(`${node.id}`); + } + // Listing identities + for (const identity of gestalt.identities) { + output.push(`${identity.providerId}:${identity.identityId}`); + } + output.push(''); + count++; + } + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandList; diff --git a/src/identities/CommandPermissions.ts b/src/identities/CommandPermissions.ts new file mode 100644 index 00000000..df8a9240 --- /dev/null +++ b/src/identities/CommandPermissions.ts @@ -0,0 +1,115 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { GestaltId } from 'polykey/dist/gestalts/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandPermissions extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('permissions'); + this.description('Gets the Permissions for a Node or Identity'); + this.argument( + '', + 'Node ID or `Provider ID:Identity ID`', + parsers.parseGestaltId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (gestaltId: GestaltId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const utils = await import('polykey/dist/utils'); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const [type, id] = gestaltId; + let actions: string[] = []; + switch (type) { + case 'node': + { + // Getting by Node + const res = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsGetByNode({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(id), + }), + auth, + ); + actions = res.actionsList; + } + break; + case 'identity': + { + // Getting by Identity + const res = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsGetByIdentity( + { + metadata: auth, + providerId: id[0], + identityId: id[1], + }, + ), + auth, + ); + actions = res.actionsList; + } + break; + default: + utils.never(); + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: { + permissions: actions, + }, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandPermissions; diff --git a/src/identities/CommandSearch.ts b/src/identities/CommandSearch.ts new file mode 100644 index 00000000..f641bcda --- /dev/null +++ b/src/identities/CommandSearch.ts @@ -0,0 +1,137 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { IdentityInfoMessage } from 'polykey/dist/client/handlers/types'; +import type { ReadableStream } from 'stream/web'; +import type { ClientRPCResponseResult } from 'polykey/dist/client/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandSearch extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('search'); + this.description('Searches a Provider for any Connected Identities'); + this.argument( + '[searchTerms...]', + 'Search parameters to apply to connected identities', + ); + this.option( + '-pi, --provider-id [providerId...]', + 'Digital identity provider(s) to search on', + ); + this.option( + '-aii, --auth-identity-id [authIdentityId]', + 'Name of your own authenticated identity to find connected identities of', + parsers.parseIdentityId, + ); + this.option( + '-ii, --identity-id [identityId]', + 'Name of the digital identity to search for', + parsers.parseIdentityId, + ); + this.option( + '-d, --disconnected', + 'Include disconnected identities in search', + ); + this.option( + '-l, --limit [number]', + 'Limit the number of search results to display to a specific number', + parsers.parseInteger, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (searchTerms, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication(async (auth) => { + let readableStream: ReadableStream< + ClientRPCResponseResult + >; + if (options.identityId) { + readableStream = + await pkClient.rpcClientClient.methods.identitiesInfoGet({ + metadata: auth, + identityId: options.identityId, + authIdentityId: options.authIdentityId, + disconnected: options.disconnected, + providerIdList: options.providerId ?? [], + searchTermList: searchTerms, + limit: options.limit, + }); + } else { + readableStream = + await pkClient.rpcClientClient.methods.identitiesInfoConnectedGet( + { + metadata: auth, + identityId: options.identityId, + authIdentityId: options.authIdentityId, + disconnected: options.disconnected, + providerIdList: options.providerId ?? [], + searchTermList: searchTerms, + limit: options.limit, + }, + ); + } + for await (const identityInfoMessage of readableStream) { + const output = { + providerId: identityInfoMessage.providerId, + identityId: identityInfoMessage.identityId, + name: identityInfoMessage.name, + email: identityInfoMessage.email, + url: identityInfoMessage.url, + }; + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: output, + }), + ); + } + }, auth); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandSearch; diff --git a/src/identities/CommandTrust.ts b/src/identities/CommandTrust.ts new file mode 100644 index 00000000..e8460be7 --- /dev/null +++ b/src/identities/CommandTrust.ts @@ -0,0 +1,104 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { GestaltId } from 'polykey/dist/gestalts/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandTrust extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('trust'); + this.description('Trust a Keynode or Identity'); + this.argument( + '', + 'Node ID or `Provider ID:Identity ID`', + parsers.parseGestaltId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (gestaltId: GestaltId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const utils = await import('polykey/dist/utils'); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const [type, id] = gestaltId; + switch (type) { + case 'node': + { + // Setting by Node. + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsGestaltTrustByNode({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(id), + }), + auth, + ); + } + break; + case 'identity': + { + // Setting by Identity + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsGestaltTrustByIdentity( + { + metadata: auth, + providerId: id[0], + identityId: id[1], + }, + ), + auth, + ); + } + break; + default: + utils.never(); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandTrust; diff --git a/src/identities/CommandUntrust.ts b/src/identities/CommandUntrust.ts new file mode 100644 index 00000000..356fc290 --- /dev/null +++ b/src/identities/CommandUntrust.ts @@ -0,0 +1,107 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { GestaltId } from 'polykey/dist/gestalts/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandUntrust extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('untrust'); + this.description('Untrust a Keynode or Identity'); + this.argument( + '', + 'Node ID or `Provider ID:Identity ID`', + parsers.parseGestaltId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (gestaltId: GestaltId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const utils = await import('polykey/dist/utils'); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const action = 'notify'; + const [type, id] = gestaltId; + switch (type) { + case 'node': + { + // Setting by Node. + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsUnsetByNode({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(id), + action, + }), + auth, + ); + } + break; + case 'identity': + { + // Setting by Identity + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.gestaltsActionsUnsetByIdentity( + { + metadata: auth, + providerId: id[0], + identityId: id[1], + action, + }, + ), + auth, + ); + } + break; + default: + utils.never(); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandUntrust; diff --git a/src/identities/index.ts b/src/identities/index.ts new file mode 100644 index 00000000..71818327 --- /dev/null +++ b/src/identities/index.ts @@ -0,0 +1 @@ +export { default } from './CommandIdentities'; diff --git a/src/keys/CommandCert.ts b/src/keys/CommandCert.ts new file mode 100644 index 00000000..66156fc8 --- /dev/null +++ b/src/keys/CommandCert.ts @@ -0,0 +1,81 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandCert extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('cert'); + this.description('Get the Root Certificate'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysCertsGet({ + metadata: auth, + }), + auth, + ); + const result = { + cert: response.cert, + }; + let output: any = result; + if (options.format === 'human') { + output = [`Root certificate:\t\t${result.cert}`]; + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandCert; diff --git a/src/keys/CommandCertchain.ts b/src/keys/CommandCertchain.ts new file mode 100644 index 00000000..c50ad3fc --- /dev/null +++ b/src/keys/CommandCertchain.ts @@ -0,0 +1,85 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandsCertchain extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('certchain'); + this.description('Get the Root Certificate Chain'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const data = await binUtils.retryAuthentication(async (auth) => { + const data: Array = []; + const stream = + await pkClient.rpcClientClient.methods.keysCertsChainGet({ + metadata: auth, + }); + for await (const cert of stream) { + data.push(cert.cert); + } + return data; + }, auth); + const result = { + certchain: data, + }; + let output: any = result; + if (options.format === 'human') { + output = [`Root Certificate Chain:\t\t${result.certchain}`]; + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandsCertchain; diff --git a/src/keys/CommandDecrypt.ts b/src/keys/CommandDecrypt.ts new file mode 100644 index 00000000..3aae9c2b --- /dev/null +++ b/src/keys/CommandDecrypt.ts @@ -0,0 +1,103 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import * as binErrors from '../errors'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandDecrypt extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('decrypt'); + this.description('Decrypt a File using the Root Keypair'); + this.argument( + '', + 'Path to the file to decrypt, file must use binary encoding', + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (filePath, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let cipherText: string; + try { + cipherText = await this.fs.promises.readFile(filePath, { + encoding: 'binary', + }); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysDecrypt({ + metadata: auth, + data: cipherText, + }), + auth, + ); + const result = { + decryptedData: response.data, + }; + let output: any = result; + if (options.format === 'human') { + output = [`Decrypted data:\t\t${result.decryptedData}`]; + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandDecrypt; diff --git a/src/keys/CommandEncrypt.ts b/src/keys/CommandEncrypt.ts new file mode 100644 index 00000000..933432bc --- /dev/null +++ b/src/keys/CommandEncrypt.ts @@ -0,0 +1,130 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { PublicKeyJWK } from 'polykey/dist/keys/types'; +import * as binErrors from '../errors'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandEncypt extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('encrypt'); + this.description('Encrypt a File for a target node'); + this.argument( + '', + 'Path to the file to encrypt, file must use binary encoding', + ); + this.argument('', 'NodeId or public JWK for target node'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (filePath, nodeIdOrJwkFile, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const keysUtils = await import('polykey/dist/keys/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let plainText: string; + try { + plainText = await this.fs.promises.readFile(filePath, { + encoding: 'binary', + }); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + let publicJWK: PublicKeyJWK; + const nodeId = nodesUtils.decodeNodeId(nodeIdOrJwkFile); + if (nodeId != null) { + publicJWK = keysUtils.publicKeyToJWK( + keysUtils.publicKeyFromNodeId(nodeId), + ); + } else { + // If it's not a NodeId then it's a file path to the JWK + try { + const rawJWK = await this.fs.promises.readFile(nodeIdOrJwkFile, { + encoding: 'utf-8', + }); + publicJWK = JSON.parse(rawJWK) as PublicKeyJWK; + // Checking if the JWK is valid + keysUtils.publicKeyFromJWK(publicJWK); + } catch (e) { + throw new binErrors.ErrorCLIPublicJWKFileRead( + 'Failed to parse JWK file', + { cause: e }, + ); + } + } + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysEncrypt({ + metadata: auth, + publicKeyJwk: publicJWK, + data: plainText, + }), + auth, + ); + const result = { + encryptedData: response.data, + }; + let output: any = result; + if (options.format === 'human') { + output = [`Encrypted data:\t\t${result.encryptedData}`]; + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandEncypt; diff --git a/src/keys/CommandKeys.ts b/src/keys/CommandKeys.ts new file mode 100644 index 00000000..bf2530a0 --- /dev/null +++ b/src/keys/CommandKeys.ts @@ -0,0 +1,35 @@ +import CommandCert from './CommandCert'; +import CommandCertchain from './CommandCertchain'; +import CommandDecrypt from './CommandDecrypt'; +import CommandEncrypt from './CommandEncrypt'; +import CommandPassword from './CommandPassword'; +import CommandRenew from './CommandRenew'; +import CommandReset from './CommandReset'; +import CommandPublic from './CommandPublic'; +import CommandPrivate from './CommandPrivate'; +import CommandKeypair from './CommandPair'; +import CommandSign from './CommandSign'; +import CommandVerify from './CommandVerify'; +import CommandPolykey from '../CommandPolykey'; + +class CommandKeys extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('keys'); + this.description('Keys Operations'); + this.addCommand(new CommandCert(...args)); + this.addCommand(new CommandCertchain(...args)); + this.addCommand(new CommandDecrypt(...args)); + this.addCommand(new CommandEncrypt(...args)); + this.addCommand(new CommandPassword(...args)); + this.addCommand(new CommandRenew(...args)); + this.addCommand(new CommandReset(...args)); + this.addCommand(new CommandPublic(...args)); + this.addCommand(new CommandPrivate(...args)); + this.addCommand(new CommandKeypair(...args)); + this.addCommand(new CommandSign(...args)); + this.addCommand(new CommandVerify(...args)); + } +} + +export default CommandKeys; diff --git a/src/keys/CommandPair.ts b/src/keys/CommandPair.ts new file mode 100644 index 00000000..3e5f9bf1 --- /dev/null +++ b/src/keys/CommandPair.ts @@ -0,0 +1,87 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandKeypair extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('keypair'); + this.description( + 'Exports the encrypted private key JWE and public key JWK', + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.addOption(binOptions.passwordNewFile); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const passwordNew = await binProcessors.processNewPassword( + options.passwordNewFile, + this.fs, + true, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const keyPairJWK = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysKeyPair({ + metadata: auth, + password: passwordNew, + }), + auth, + ); + const pair = { + publicKey: keyPairJWK.publicKeyJwk, + privateKey: keyPairJWK.privateKeyJwe, + }; + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: pair, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandKeypair; diff --git a/src/keys/CommandPassword.ts b/src/keys/CommandPassword.ts new file mode 100644 index 00000000..171b361e --- /dev/null +++ b/src/keys/CommandPassword.ts @@ -0,0 +1,75 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandPassword extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('password'); + this.description('Change the Password for the Root Keypair'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.addOption(binOptions.passwordNewFile); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const passwordNew = await binProcessors.processNewPassword( + options.passwordNewFile, + this.fs, + true, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysPasswordChange({ + metadata: auth, + password: passwordNew, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandPassword; diff --git a/src/keys/CommandPrivate.ts b/src/keys/CommandPrivate.ts new file mode 100644 index 00000000..8495508c --- /dev/null +++ b/src/keys/CommandPrivate.ts @@ -0,0 +1,82 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandPrivate extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('private'); + this.description('Exports the encrypted private key JWE'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.addOption(binOptions.passwordNewFile); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const passwordNew = await binProcessors.processNewPassword( + options.passwordNewFile, + this.fs, + true, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const keyPairJWK = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysKeyPair({ + metadata: auth, + password: passwordNew, + }), + auth, + ); + const privateKeyJWE = keyPairJWK.privateKeyJwe; + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: privateKeyJWE, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandPrivate; diff --git a/src/keys/CommandPublic.ts b/src/keys/CommandPublic.ts new file mode 100644 index 00000000..8341fc00 --- /dev/null +++ b/src/keys/CommandPublic.ts @@ -0,0 +1,75 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandPublic extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('public'); + this.description('Exports the encrypted private key JWE'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const keyPairJWK = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysPublicKey({ + metadata: auth, + }), + auth, + ); + const publicKeyJWK = keyPairJWK.publicKeyJwk; + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: publicKeyJWK, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandPublic; diff --git a/src/keys/CommandRenew.ts b/src/keys/CommandRenew.ts new file mode 100644 index 00000000..3e11c65d --- /dev/null +++ b/src/keys/CommandRenew.ts @@ -0,0 +1,75 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandRenew extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('renew'); + this.description('Renew the Root Keypair'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.addOption(binOptions.passwordNewFile); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const passwordNew = await binProcessors.processNewPassword( + options.passwordNewFile, + this.fs, + true, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysKeyPairRenew({ + metadata: auth, + password: passwordNew, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandRenew; diff --git a/src/keys/CommandReset.ts b/src/keys/CommandReset.ts new file mode 100644 index 00000000..11880fc5 --- /dev/null +++ b/src/keys/CommandReset.ts @@ -0,0 +1,75 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandReset extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('reset'); + this.description('Reset the Root Keypair'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.addOption(binOptions.passwordNewFile); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const passwordNew = await binProcessors.processNewPassword( + options.passwordNewFile, + this.fs, + true, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysKeyPairReset({ + metadata: auth, + password: passwordNew, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandReset; diff --git a/src/keys/CommandSign.ts b/src/keys/CommandSign.ts new file mode 100644 index 00000000..963ec804 --- /dev/null +++ b/src/keys/CommandSign.ts @@ -0,0 +1,103 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import * as binErrors from '../errors'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandSign extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('sign'); + this.description('Sign a File using the Root Keypair'); + this.argument( + '', + 'Path to the file to sign, file must use binary encoding', + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (filePath, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let data: string; + try { + data = await this.fs.promises.readFile(filePath, { + encoding: 'binary', + }); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysSign({ + metadata: auth, + data, + }), + auth, + ); + const result = { + signature: response.signature, + }; + let output: any = result; + if (options.format === 'human') { + output = [`Signature:\t\t${result.signature}`]; + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandSign; diff --git a/src/keys/CommandVerify.ts b/src/keys/CommandVerify.ts new file mode 100644 index 00000000..840ec184 --- /dev/null +++ b/src/keys/CommandVerify.ts @@ -0,0 +1,139 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { PublicKeyJWK } from 'polykey/dist/keys/types'; +import * as binErrors from '../errors'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandVerify extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('verify'); + this.description('Verify a Signature for a target node'); + this.argument( + '', + 'Path to the file to verify, file must use binary encoding', + ); + this.argument( + '', + 'Path to the signature to be verified, file must be binary encoded', + ); + this.argument('', 'NodeId or public JWK for target node'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (filePath, signaturePath, nodeIdOrJwkFile, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const keysUtils = await import('polykey/dist/keys/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let data: string; + let signature: string; + try { + data = await this.fs.promises.readFile(filePath, { + encoding: 'binary', + }); + signature = await this.fs.promises.readFile(signaturePath, { + encoding: 'binary', + }); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + let publicJWK: PublicKeyJWK; + const nodeId = nodesUtils.decodeNodeId(nodeIdOrJwkFile); + if (nodeId != null) { + publicJWK = keysUtils.publicKeyToJWK( + keysUtils.publicKeyFromNodeId(nodeId), + ); + } else { + // If it's not a NodeId then it's a file path to the JWK + try { + const rawJWK = await this.fs.promises.readFile(nodeIdOrJwkFile, { + encoding: 'utf-8', + }); + publicJWK = JSON.parse(rawJWK) as PublicKeyJWK; + // Checking if the JWK is valid + keysUtils.publicKeyFromJWK(publicJWK); + } catch (e) { + throw new binErrors.ErrorCLIPublicJWKFileRead( + 'Failed to parse JWK file', + { cause: e }, + ); + } + } + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.keysVerify({ + metadata: auth, + publicKeyJwk: publicJWK, + data, + signature, + }), + auth, + ); + const result = { + signatureVerified: response.success, + }; + let output: any = result; + if (options.format === 'human') { + output = [`Signature verified:\t\t${result.signatureVerified}`]; + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandVerify; diff --git a/src/keys/index.ts b/src/keys/index.ts new file mode 100644 index 00000000..11736eb1 --- /dev/null +++ b/src/keys/index.ts @@ -0,0 +1 @@ +export { default } from './CommandKeys'; diff --git a/src/nodes/CommandAdd.ts b/src/nodes/CommandAdd.ts new file mode 100644 index 00000000..31851dc6 --- /dev/null +++ b/src/nodes/CommandAdd.ts @@ -0,0 +1,82 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import type { Host, Port } from 'polykey/dist/network/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils/utils'; +import * as binProcessors from '../utils/processors'; +import * as binOptions from '../utils/options'; +import * as binParsers from '../utils/parsers'; + +class CommandAdd extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('add'); + this.description('Add a Node to the Node Graph'); + this.argument('', 'Id of the node to add', binParsers.parseNodeId); + this.argument('', 'Address of the node', binParsers.parseHost); + this.argument('', 'Port of the node', binParsers.parsePort); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.addOption(binOptions.forceNodeAdd); + this.addOption(binOptions.noPing); + this.action(async (nodeId: NodeId, host: Host, port: Port, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.nodesAdd({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + host: host, + port: port, + force: options.force, + ping: options.ping, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandAdd; diff --git a/src/nodes/CommandClaim.ts b/src/nodes/CommandClaim.ts new file mode 100644 index 00000000..8321a2ba --- /dev/null +++ b/src/nodes/CommandClaim.ts @@ -0,0 +1,106 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; + +class CommandClaim extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('claim'); + this.description('Claim another Keynode'); + this.argument( + '', + 'Id of the node to claim', + binParsers.parseNodeId, + ); + this.option( + '-f, --force-invite', + '(optional) Flag to force a Gestalt Invitation to be sent rather than a node claim.', + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (nodeId: NodeId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.nodesClaim({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + forceInvite: options.forceInvite, + }), + auth, + ); + const claimed = response.success; + if (claimed) { + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: [ + `Successfully generated a cryptolink claim on Keynode with ID ${nodesUtils.encodeNodeId( + nodeId, + )}`, + ], + }), + ); + } else { + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: [ + `Successfully sent Gestalt Invite notification to Keynode with ID ${nodesUtils.encodeNodeId( + nodeId, + )}`, + ], + }), + ); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandClaim; diff --git a/src/nodes/CommandConnections.ts b/src/nodes/CommandConnections.ts new file mode 100644 index 00000000..dd2c458c --- /dev/null +++ b/src/nodes/CommandConnections.ts @@ -0,0 +1,97 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeConnectionMessage } from 'polykey/dist/client/handlers/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils/utils'; +import * as binProcessors from '../utils/processors'; + +class CommandAdd extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('connections'); + this.description('list all active node connections'); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + // DO things here... + // Like create the message. + const connections = await binUtils.retryAuthentication(async (auth) => { + const connections = + await pkClient.rpcClientClient.methods.nodesListConnections({ + metadata: auth, + }); + const connectionEntries: Array = []; + for await (const connection of connections) { + connectionEntries.push(connection); + } + return connectionEntries; + }, auth); + if (options.format === 'human') { + const output: Array = []; + for (const connection of connections) { + const hostnameString = + connection.hostname === '' ? '' : `(${connection.hostname})`; + const hostString = `${connection.nodeIdEncoded}@${connection.host}${hostnameString}:${connection.port}`; + const usageCount = connection.usageCount; + const timeout = + connection.timeout === -1 ? 'NA' : `${connection.timeout}`; + const outputLine = `${hostString}\t${usageCount}\t${timeout}`; + output.push(outputLine); + } + process.stdout.write( + binUtils.outputFormatter({ + type: 'list', + data: output, + }), + ); + } else { + process.stdout.write( + binUtils.outputFormatter({ + type: 'json', + data: connections, + }), + ); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandAdd; diff --git a/src/nodes/CommandFind.ts b/src/nodes/CommandFind.ts new file mode 100644 index 00000000..bbd1d5e2 --- /dev/null +++ b/src/nodes/CommandFind.ts @@ -0,0 +1,118 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import type { Host, Port } from 'polykey/dist/network/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; +import * as binErrors from '../errors'; + +class CommandFind extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('find'); + this.description('Attempt to Find a Node'); + this.argument('', 'Id of the node to find', binParsers.parseNodeId); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (nodeId: NodeId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const networkUtils = await import('polykey/dist/network/utils'); + const nodesErrors = await import('polykey/dist/nodes/errors'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const result = { + success: false, + message: '', + id: '', + host: '', + port: 0, + }; + try { + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.nodesFind({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + }), + auth, + ); + result.success = true; + result.id = nodesUtils.encodeNodeId(nodeId); + result.host = response.host; + result.port = response.port; + result.message = `Found node at ${networkUtils.buildAddress( + result.host as Host, + result.port as Port, + )}`; + } catch (err) { + if ( + !(err.cause instanceof nodesErrors.ErrorNodeGraphNodeIdNotFound) + ) { + throw err; + } + // Else failed to find the node. + result.success = false; + result.id = nodesUtils.encodeNodeId(nodeId); + result.host = ''; + result.port = 0; + result.message = `Failed to find node ${result.id}`; + } + let output: any = result; + if (options.format === 'human') output = [result.message]; + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + // Like ping it should error when failing to find node for automation reasons. + if (!result.success) { + throw new binErrors.ErrorCLINodeFindFailed(result.message); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandFind; diff --git a/src/nodes/CommandGetAll.ts b/src/nodes/CommandGetAll.ts new file mode 100644 index 00000000..94b17aab --- /dev/null +++ b/src/nodes/CommandGetAll.ts @@ -0,0 +1,84 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandGetAll extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('getall'); + this.description('Get all Nodes from Node Graph'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const result = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.nodesGetAll({ + metadata: auth, + }), + auth, + ); + let output: Array = []; + for await (const nodesGetMessage of result) { + output.push(nodesGetMessage); + } + if (options.format === 'human') { + output = output.map( + (value) => + `NodeId ${value.nodeIdEncoded}, Address ${value.host}:${value.port}, bucketIndex ${value.bucketIndex}`, + ); + } + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandGetAll; diff --git a/src/nodes/CommandNodes.ts b/src/nodes/CommandNodes.ts new file mode 100644 index 00000000..145aeee3 --- /dev/null +++ b/src/nodes/CommandNodes.ts @@ -0,0 +1,23 @@ +import CommandAdd from './CommandAdd'; +import CommandClaim from './CommandClaim'; +import CommandFind from './CommandFind'; +import CommandPing from './CommandPing'; +import CommandGetAll from './CommandGetAll'; +import CommandConnections from './CommandConnections'; +import CommandPolykey from '../CommandPolykey'; + +class CommandNodes extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('nodes'); + this.description('Nodes Operations'); + this.addCommand(new CommandAdd(...args)); + this.addCommand(new CommandClaim(...args)); + this.addCommand(new CommandFind(...args)); + this.addCommand(new CommandPing(...args)); + this.addCommand(new CommandGetAll(...args)); + this.addCommand(new CommandConnections(...args)); + } +} + +export default CommandNodes; diff --git a/src/nodes/CommandPing.ts b/src/nodes/CommandPing.ts new file mode 100644 index 00000000..f4a8d691 --- /dev/null +++ b/src/nodes/CommandPing.ts @@ -0,0 +1,91 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; +import * as binErrors from '../errors'; + +class CommandPing extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('ping'); + this.description("Ping a Node to check if it's Online"); + this.argument('', 'Id of the node to ping', binParsers.parseNodeId); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (nodeId: NodeId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let error; + const statusMessage = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.nodesPing({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + }), + auth, + ); + const status = { success: false, message: '' }; + status.success = statusMessage ? statusMessage.success : false; + if (!status.success && !error) { + error = new binErrors.ErrorCLINodePingFailed('No response received'); + } + if (status.success) status.message = 'Node is Active.'; + else status.message = error.message; + const output: any = + options.format === 'json' ? status : [status.message]; + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: output, + }), + ); + if (error != null) throw error; + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandPing; diff --git a/src/nodes/index.ts b/src/nodes/index.ts new file mode 100644 index 00000000..b604b303 --- /dev/null +++ b/src/nodes/index.ts @@ -0,0 +1 @@ +export { default } from './CommandNodes'; diff --git a/src/notifications/CommandClear.ts b/src/notifications/CommandClear.ts new file mode 100644 index 00000000..61046c11 --- /dev/null +++ b/src/notifications/CommandClear.ts @@ -0,0 +1,68 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandClear extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('clear'); + this.description('Clear all Notifications'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.notificationsClear({ + metadata: auth, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandClear; diff --git a/src/notifications/CommandNotifications.ts b/src/notifications/CommandNotifications.ts new file mode 100644 index 00000000..aff48da1 --- /dev/null +++ b/src/notifications/CommandNotifications.ts @@ -0,0 +1,17 @@ +import CommandClear from './CommandClear'; +import CommandRead from './CommandRead'; +import CommandSend from './CommandSend'; +import CommandPolykey from '../CommandPolykey'; + +class CommandNotifications extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('notifications'); + this.description('Notifications Operations'); + this.addCommand(new CommandClear(...args)); + this.addCommand(new CommandRead(...args)); + this.addCommand(new CommandSend(...args)); + } +} + +export default CommandNotifications; diff --git a/src/notifications/CommandRead.ts b/src/notifications/CommandRead.ts new file mode 100644 index 00000000..b440da5d --- /dev/null +++ b/src/notifications/CommandRead.ts @@ -0,0 +1,104 @@ +import type { Notification } from 'polykey/dist/notifications/types'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandRead extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('read'); + this.description('Display Notifications'); + this.option( + '-u, --unread', + '(optional) Flag to only display unread notifications', + ); + this.option( + '-n, --number [number]', + '(optional) Number of notifications to read', + 'all', + ); + this.option( + '-o, --order [order]', + '(optional) Order to read notifications', + 'newest', + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const notificationsUtils = await import( + 'polykey/dist/notifications/utils' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.notificationsRead({ + metadata: auth, + unread: options.unread, + number: options.number, + order: options.order, + }), + meta, + ); + const notifications: Array = []; + for await (const notificationMessage of response) { + const notification = notificationsUtils.parseNotification( + notificationMessage.notification, + ); + notifications.push(notification); + } + for (const notification of notifications) { + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: notification, + }), + ); + } + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandRead; diff --git a/src/notifications/CommandSend.ts b/src/notifications/CommandSend.ts new file mode 100644 index 00000000..da001ff6 --- /dev/null +++ b/src/notifications/CommandSend.ts @@ -0,0 +1,79 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; + +class CommandSend extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('send'); + this.description('Send a Notification with a Message to another Node'); + this.argument( + '', + 'Id of the node to send a message to', + binParsers.parseNodeId, + ); + this.argument('', 'Message to send'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (nodeId: NodeId, message, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.notificationsSend({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + message: message, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandSend; diff --git a/src/notifications/index.ts b/src/notifications/index.ts new file mode 100644 index 00000000..67ef993d --- /dev/null +++ b/src/notifications/index.ts @@ -0,0 +1 @@ +export { default } from './CommandNotifications'; diff --git a/src/polykey-agent.ts b/src/polykey-agent.ts new file mode 100755 index 00000000..334bd404 --- /dev/null +++ b/src/polykey-agent.ts @@ -0,0 +1,141 @@ +#!/usr/bin/env node +/** + * The is an internal script for running the PolykeyAgent as a child process + * This is not to be exported for external execution + * @module + */ +import type { AgentChildProcessInput, AgentChildProcessOutput } from './types'; +import fs from 'fs'; +import process from 'process'; +/** + * Hack for wiping out the threads signal handlers + * See: https://github.com/andywer/threads.js/issues/388 + * This is done statically during this import + * It is essential that the threads import here is very first import of threads module + * in the entire codebase for this hack to work + * If the worker manager is used, it must be stopped gracefully with the PolykeyAgent + */ +import 'threads'; +process.removeAllListeners('SIGINT'); +process.removeAllListeners('SIGTERM'); +import Logger, { StreamHandler, formatting } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import ErrorPolykey from 'polykey/dist/ErrorPolykey'; +import { promisify, promise } from 'polykey/dist/utils'; +import * as binUtils from './utils'; + +process.title = 'polykey-agent'; + +const logger = new Logger('polykey', undefined, [new StreamHandler()]); + +/** + * Starts the agent process + */ +async function main(_argv = process.argv): Promise { + const exitHandlers = new binUtils.ExitHandlers(); + const processSend = promisify(process.send!.bind(process)); + const { p: messageInP, resolveP: resolveMessageInP } = + promise(); + process.once('message', (data: AgentChildProcessInput) => { + resolveMessageInP(data); + }); + const messageIn = await messageInP; + const errFormat = messageIn.format === 'json' ? 'json' : 'error'; + exitHandlers.errFormat = errFormat; + // Set the logger according to the verbosity + logger.setLevel(messageIn.logLevel); + // Set the logger formatter according to the format + if (messageIn.format === 'json') { + logger.handlers.forEach((handler) => + handler.setFormatter(formatting.jsonFormatter), + ); + } + let pkAgent: PolykeyAgent; + exitHandlers.handlers.push(async () => { + await pkAgent?.stop(); + }); + try { + pkAgent = await PolykeyAgent.createPolykeyAgent({ + fs, + logger: logger.getChild(PolykeyAgent.name), + ...messageIn.agentConfig, + }); + } catch (e) { + if (e instanceof ErrorPolykey) { + process.stderr.write( + binUtils.outputFormatter({ + type: errFormat, + data: e, + }), + ); + process.exitCode = e.exitCode; + } else { + // Unknown error, this should not happen + process.stderr.write( + binUtils.outputFormatter({ + type: errFormat, + data: e, + }), + ); + process.exitCode = 255; + } + const messageOut: AgentChildProcessOutput = { + status: 'FAILURE', + error: { + name: e.name, + description: e.description, + message: e.message, + exitCode: e.exitCode, + data: e.data, + stack: e.stack, + }, + }; + try { + await processSend(messageOut); + } catch (e) { + // If processSend itself failed here + // There's no point attempting to propagate the error to the parent + process.stderr.write( + binUtils.outputFormatter({ + type: errFormat, + data: e, + }), + ); + process.exitCode = 255; + } + return process.exitCode; + } + const messageOut: AgentChildProcessOutput = { + status: 'SUCCESS', + recoveryCode: pkAgent.keyRing.recoveryCode, + pid: process.pid, + nodeId: nodesUtils.encodeNodeId(pkAgent.keyRing.getNodeId()), + clientHost: pkAgent.webSocketServerClient.getHost(), + clientPort: pkAgent.webSocketServerClient.getPort(), + agentHost: pkAgent.quicServerAgent.host, + agentPort: pkAgent.quicServerAgent.port, + }; + try { + await processSend(messageOut); + } catch (e) { + // If processSend itself failed here + // There's no point attempting to propagate the error to the parent + process.stderr.write( + binUtils.outputFormatter({ + type: errFormat, + data: e, + }), + ); + process.exitCode = 255; + return process.exitCode; + } + process.exitCode = 0; + return process.exitCode; +} + +if (require.main === module) { + void main(); +} + +export default main; diff --git a/src/polykey.ts b/src/polykey.ts new file mode 100755 index 00000000..387cd43d --- /dev/null +++ b/src/polykey.ts @@ -0,0 +1,105 @@ +#!/usr/bin/env node + +import fs from 'fs'; +import process from 'process'; +/** + * Hack for wiping out the threads signal handlers + * See: https://github.com/andywer/threads.js/issues/388 + * This is done statically during this import + * It is essential that the threads import here is very first import of threads module + * in the entire codebase for this hack to work + * If the worker manager is used, it must be stopped gracefully with the PolykeyAgent + */ +import 'threads'; +process.removeAllListeners('SIGINT'); +process.removeAllListeners('SIGTERM'); +import commander from 'commander'; +import ErrorPolykey from 'polykey/dist/ErrorPolykey'; +import config from 'polykey/dist/config'; +import CommandBootstrap from './bootstrap'; +import CommandAgent from './agent'; +import CommandVaults from './vaults'; +import CommandSecrets from './secrets'; +import CommandKeys from './keys'; +import CommandNodes from './nodes'; +import CommandIdentities from './identities'; +import CommandNotifications from './notifications'; +import CommandPolykey from './CommandPolykey'; +import * as binUtils from './utils'; + +process.title = 'polykey'; + +async function main(argv = process.argv): Promise { + // Registers signal and process error handler + // Any resource cleanup must be resolved within their try-catch block + // Leaf commands may register exit handlers in case of signal exits + // Process error handler should only be used by non-terminating commands + // When testing, this entire must be mocked to be a noop + const exitHandlers = new binUtils.ExitHandlers(); + const rootCommand = new CommandPolykey({ exitHandlers, fs }); + rootCommand.name('polykey'); + rootCommand.version(config.sourceVersion); + rootCommand.description('Polykey CLI'); + rootCommand.addCommand(new CommandBootstrap({ exitHandlers, fs })); + rootCommand.addCommand(new CommandAgent({ exitHandlers, fs })); + rootCommand.addCommand(new CommandNodes({ exitHandlers, fs })); + rootCommand.addCommand(new CommandSecrets({ exitHandlers, fs })); + rootCommand.addCommand(new CommandKeys({ exitHandlers, fs })); + rootCommand.addCommand(new CommandVaults({ exitHandlers, fs })); + rootCommand.addCommand(new CommandIdentities({ exitHandlers, fs })); + rootCommand.addCommand(new CommandNotifications({ exitHandlers, fs })); + try { + // `argv` will have node path and the script path as the first 2 parameters + // navigates and executes the subcommand + await rootCommand.parseAsync(argv); + // Successful execution (even if the command was non-terminating) + process.exitCode = 0; + } catch (e) { + const errFormat = rootCommand.opts().format === 'json' ? 'json' : 'error'; + if (e instanceof commander.CommanderError) { + // Commander writes help and error messages on stderr automatically + if ( + e.code === 'commander.help' || + e.code === 'commander.helpDisplayed' || + e.code === 'commander.version' + ) { + process.exitCode = 0; + } else { + // Other commander codes: + // commander.unknownOption + // commander.unknownCommand + // commander.invalidArgument + // commander.excessArguments + // commander.missingArgument + // commander.missingMandatoryOptionValue + // commander.optionMissingArgument + // use 64 for EX_USAGE + process.exitCode = 64; + } + } else if (e instanceof ErrorPolykey) { + process.stderr.write( + binUtils.outputFormatter({ + type: errFormat, + data: e, + }), + ); + process.exitCode = e.exitCode; + } else { + // Unknown error, this should not happen + process.stderr.write( + binUtils.outputFormatter({ + type: errFormat, + data: e, + }), + ); + process.exitCode = 255; + } + } + return process.exitCode ?? 255; +} + +if (require.main === module) { + void main(); +} + +export default main; diff --git a/src/secrets/CommandCreate.ts b/src/secrets/CommandCreate.ts new file mode 100644 index 00000000..f3b60796 --- /dev/null +++ b/src/secrets/CommandCreate.ts @@ -0,0 +1,96 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import * as binErrors from '../errors'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandCreate extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('create'); + this.description('Create a Secret within a given Vault'); + this.argument( + '', + 'On disk path to the secret file with the contents of the new secret', + ); + this.argument( + '', + 'Path to the secret to be created, specified as :', + parsers.parseSecretPath, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (directoryPath, secretPath, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let content: Buffer; + try { + content = await this.fs.promises.readFile(directoryPath); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsNew({ + metadata: auth, + nameOrId: secretPath[0], + secretName: secretPath[1], + secretContent: content.toString('binary'), + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandCreate; diff --git a/src/secrets/CommandDelete.ts b/src/secrets/CommandDelete.ts new file mode 100644 index 00000000..60841856 --- /dev/null +++ b/src/secrets/CommandDelete.ts @@ -0,0 +1,77 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandDelete extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('delete'); + this.aliases(['del', 'rm']); + this.description('Delete a Secret from a Specified Vault'); + this.argument( + '', + 'Path to the secret that to be deleted, specified as :', + parsers.parseSecretPath, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (secretPath, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsDelete({ + metadata: auth, + nameOrId: secretPath[0], + secretName: secretPath[1], + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandDelete; diff --git a/src/secrets/CommandDir.ts b/src/secrets/CommandDir.ts new file mode 100644 index 00000000..b86f29f5 --- /dev/null +++ b/src/secrets/CommandDir.ts @@ -0,0 +1,75 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandDir extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('dir'); + this.description('Add a Directory of Secrets within a Given Vault'); + this.argument( + '', + 'On disk path to the directory containing the secrets to be added', + ); + this.argument('', 'Name of the vault to add the secrets to'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (directoryPath, vaultName, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsNewDir({ + metadata: auth, + nameOrId: vaultName, + dirName: directoryPath, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandDir; diff --git a/src/secrets/CommandEdit.ts b/src/secrets/CommandEdit.ts new file mode 100644 index 00000000..53797a29 --- /dev/null +++ b/src/secrets/CommandEdit.ts @@ -0,0 +1,108 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import * as binErrors from '../errors'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandEdit extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('edit'); + this.description('Edit a Secret'); + this.argument( + '', + 'Path to the secret to be edited, specified as :', + parsers.parseSecretPath, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (secretPath, options) => { + const os = await import('os'); + const { execSync } = await import('child_process'); + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsGet({ + metadata: auth, + nameOrId: secretPath[0], + secretName: secretPath[1], + }), + meta, + ); + const secretContent = response.secretContent; + // Linux + const tmpDir = `${os.tmpdir}/pksecret`; + await this.fs.promises.mkdir(tmpDir); + const tmpFile = `${tmpDir}/pkSecretFile`; + await this.fs.promises.writeFile(tmpFile, secretContent); + execSync(`$EDITOR \"${tmpFile}\"`, { stdio: 'inherit' }); + let content: Buffer; + try { + content = await this.fs.promises.readFile(tmpFile); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + await pkClient.rpcClientClient.methods.vaultsSecretsEdit({ + nameOrId: secretPath[0], + secretName: secretPath[1], + secretContent: content.toString('binary'), + }); + await this.fs.promises.rmdir(tmpDir, { recursive: true }); + // Windows + // TODO: complete windows impl + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandEdit; diff --git a/src/secrets/CommandEnv.ts b/src/secrets/CommandEnv.ts new file mode 100644 index 00000000..48014650 --- /dev/null +++ b/src/secrets/CommandEnv.ts @@ -0,0 +1,179 @@ +// Import { spawn } from 'child_process'; +// import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +// import * as vaultsPB from 'polykey/dist/proto/js/polykey/v1/vaults/vaults_pb'; +// import * as secretsPB from 'polykey/dist/proto/js/polykey/v1/secrets/secrets_pb'; +// import PolykeyClient from 'polykey/dist/PolykeyClient'; +// import * as utils from 'polykey/dist/utils'; +// import * as binUtils from '../utils'; +// import * as CLIErrors from '../errors'; +// import * as grpcErrors from 'polykey/dist/grpc/errors'; + +// import CommandPolykey from '../CommandPolykey'; +// import * as binOptions from '../utils/options'; + +// class CommandEnv extends CommandPolykey { +// constructor(...args: ConstructorParameters) { +// super(...args); +// this.name('env'); +// this.description('Secrets Env'); +// this.option( +// '--command ', +// 'In the environment of the derivation, run the shell command cmd in an interactive shell (Use --run to use a non-interactive shell instead)', +// ); +// this.option( +// '--run ', +// 'In the environment of the derivation, run the shell command cmd in a non-interactive shell, meaning (among other things) that if you hit Ctrl-C while the command is running, the shell exits (Use --command to use an interactive shell instead)', +// ); +// this.arguments( +// "Secrets to inject into env, of the format ':[=]', you can also control what the environment variable will be called using '[]' (defaults to upper, snake case of the original secret name)", +// ); +// this.addOption(binOptions.nodeId); +// this.addOption(binOptions.clientHost); +// this.addOption(binOptions.clientPort); +// this.action(async (options, command) => { + +// }); +// } +// } + +// export default CommandEnv; + +// OLD COMMAND +// const env = binUtils.createCommand('env', { +// description: 'Runs a modified environment with injected secrets', +// nodePath: true, +// verbose: true, +// format: true, +// }); +// env.option( +// '--command ', +// 'In the environment of the derivation, run the shell command cmd in an interactive shell (Use --run to use a non-interactive shell instead)', +// ); +// env.option( +// '--run ', +// 'In the environment of the derivation, run the shell command cmd in a non-interactive shell, meaning (among other things) that if you hit Ctrl-C while the command is running, the shell exits (Use --command to use an interactive shell instead)', +// ); +// env.arguments( +// "Secrets to inject into env, of the format ':[=]', you can also control what the environment variable will be called using '[]' (defaults to upper, snake case of the original secret name)", +// ); +// env.action(async (options, command) => { +// const clientConfig = {}; +// clientConfig['logger'] = new Logger('CLI Logger', LogLevel.WARN, [ +// new StreamHandler(), +// ]); +// if (options.verbose) { +// clientConfig['logger'].setLevel(LogLevel.DEBUG); +// } +// clientConfig['nodePath'] = options.nodePath +// ? options.nodePath +// : utils.getDefaultNodePath(); + +// const client = await PolykeyClient.createPolykeyClient(clientConfig); +// const vaultMessage = new vaultsPB.Vault(); +// const secretMessage = new secretsPB.Secret(); +// secretMessage.setVault(vaultMessage); +// const secretPathList: string[] = Array.from(command.args.values()); + +// try { +// if (secretPathList.length < 1) { +// throw new CLIErrors.ErrorSecretsUndefined(); +// } + +// const parsedPathList: { +// vaultName: string; +// secretName: string; +// variableName: string; +// }[] = []; + +// for (const path of secretPathList) { +// if (!binUtils.pathRegex.test(path)) { +// throw new CLIErrors.ErrorSecretPathFormat(); +// } + +// const [, vaultName, secretName, variableName] = path.match( +// binUtils.pathRegex, +// )!; +// parsedPathList.push({ +// vaultName, +// secretName, +// variableName: +// variableName ?? secretName.toUpperCase().replace('-', '_'), +// }); +// } + +// const secretEnv = { ...process.env }; + +// await client.start({}); +// const grpcClient = client.grpcClient; + +// for (const obj of parsedPathList) { +// vaultMessage.setNameOrId(obj.vaultName); +// secretMessage.setSecretName(obj.secretName); +// const res = await binUtils.unaryCallCARL( +// client, +// attemptUnaryCall(client, grpcClient.vaultsSecretsGet), +// )(secretMessage); + +// const secret = res.getSecretName(); +// secretEnv[obj.variableName] = secret; +// } + +// const shellPath = process.env.SHELL ?? 'sh'; +// const args: string[] = []; + +// if (options.command && options.run) { +// throw new CLIErrors.ErrorInvalidArguments( +// 'Only one of --command or --run can be specified', +// ); +// } else if (options.command) { +// args.push('-i'); +// args.push('-c'); +// args.push(`"${options.command}"`); +// } else if (options.run) { +// args.push('-c'); +// args.push(`"${options.run}"`); +// } + +// const shell = spawn(shellPath, args, { +// stdio: 'inherit', +// env: secretEnv, +// shell: true, +// }); + +// shell.on('close', (code) => { +// if (code !== 0) { +// process.stdout.write( +// binUtils.outputFormatter({ +// type: options.format === 'json' ? 'json' : 'list', +// data: [`Terminated with ${code}`], +// }), +// ); +// } +// }); +// } catch (err) { +// if (err instanceof grpcErrors.ErrorGRPCClientTimeout) { +// process.stderr.write(`${err.message}\n`); +// } +// if (err instanceof grpcErrors.ErrorGRPCServerNotStarted) { +// process.stderr.write(`${err.message}\n`); +// } else { +// process.stderr.write( +// binUtils.outputFormatter({ +// type: 'error', +// description: err.description, +// message: err.message, +// }), +// ); +// throw err; +// } +// } finally { +// await client.stop(); +// options.nodePath = undefined; +// options.verbose = undefined; +// options.format = undefined; +// options.command = undefined; +// options.run = undefined; +// } +// }); + +// export default env; diff --git a/src/secrets/CommandGet.ts b/src/secrets/CommandGet.ts new file mode 100644 index 00000000..c53e48e3 --- /dev/null +++ b/src/secrets/CommandGet.ts @@ -0,0 +1,83 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandGet extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('get'); + this.description('Retrieve a Secret from the Given Vault'); + this.argument( + '', + 'Path to where the secret to be retrieved, specified as :', + parsers.parseSecretPath, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (secretPath, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsGet({ + metadata: auth, + nameOrId: secretPath[0], + secretName: secretPath[1], + }), + meta, + ); + const secretContent = Buffer.from(response.secretContent, 'binary'); + process.stdout.write( + binUtils.outputFormatter({ + type: 'raw', + data: secretContent, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandGet; diff --git a/src/secrets/CommandList.ts b/src/secrets/CommandList.ts new file mode 100644 index 00000000..a52a9ff8 --- /dev/null +++ b/src/secrets/CommandList.ts @@ -0,0 +1,81 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandList extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('list'); + this.aliases(['ls']); + this.description('List all Available Secrets for a Vault'); + this.argument('', 'Name of the vault to list secrets from'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vaultName, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const data = await binUtils.retryAuthentication(async (auth) => { + const data: Array = []; + const stream = + await pkClient.rpcClientClient.methods.vaultsSecretsList({ + metadata: auth, + nameOrId: vaultName, + }); + for await (const secret of stream) { + data.push(secret.secretName); + } + return data; + }, auth); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: data, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandList; diff --git a/src/secrets/CommandMkdir.ts b/src/secrets/CommandMkdir.ts new file mode 100644 index 00000000..c5ae3a44 --- /dev/null +++ b/src/secrets/CommandMkdir.ts @@ -0,0 +1,78 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandMkdir extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('mkdir'); + this.description('Create a Directory within a Vault'); + this.argument( + '', + 'Path to where the directory to be created, specified as :', + parsers.parseSecretPath, + ); + this.option('-r, --recursive', 'Create the directory recursively'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (secretPath, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsMkdir({ + metadata: auth, + nameOrId: secretPath[0], + dirName: secretPath[1], + recursive: options.recursive, + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandMkdir; diff --git a/src/secrets/CommandRename.ts b/src/secrets/CommandRename.ts new file mode 100644 index 00000000..5f10ce14 --- /dev/null +++ b/src/secrets/CommandRename.ts @@ -0,0 +1,78 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandRename extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('rename'); + this.description('Rename a Secret'); + this.argument( + '', + 'Path to where the secret to be renamed, specified as :', + parsers.parseSecretPath, + ); + this.argument('', 'New name of the secret'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (secretPath, newSecretName, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsRename({ + metadata: auth, + nameOrId: secretPath[0], + secretName: secretPath[1], + newSecretName: newSecretName, + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandRename; diff --git a/src/secrets/CommandSecrets.ts b/src/secrets/CommandSecrets.ts new file mode 100644 index 00000000..0cf1c766 --- /dev/null +++ b/src/secrets/CommandSecrets.ts @@ -0,0 +1,33 @@ +import CommandCreate from './CommandCreate'; +import CommandDelete from './CommandDelete'; +import CommandDir from './CommandDir'; +import CommandEdit from './CommandEdit'; +// Import CommandEnv from './CommandEnv'; +import CommandGet from './CommandGet'; +import CommandList from './CommandList'; +import CommandMkdir from './CommandMkdir'; +import CommandRename from './CommandRename'; +import CommandUpdate from './CommandUpdate'; +import commandStat from './CommandStat'; +import CommandPolykey from '../CommandPolykey'; + +class CommandSecrets extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('secrets'); + this.description('Secrets Operations'); + this.addCommand(new CommandCreate(...args)); + this.addCommand(new CommandDelete(...args)); + this.addCommand(new CommandDir(...args)); + this.addCommand(new CommandEdit(...args)); + // This.addCommand(new CommandEnv(...args)); + this.addCommand(new CommandGet(...args)); + this.addCommand(new CommandList(...args)); + this.addCommand(new CommandMkdir(...args)); + this.addCommand(new CommandRename(...args)); + this.addCommand(new CommandUpdate(...args)); + this.addCommand(new commandStat(...args)); + } +} + +export default CommandSecrets; diff --git a/src/secrets/CommandStat.ts b/src/secrets/CommandStat.ts new file mode 100644 index 00000000..0ab3fba7 --- /dev/null +++ b/src/secrets/CommandStat.ts @@ -0,0 +1,90 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import * as binProcessors from '../utils/processors'; +import * as parsers from '../utils/parsers'; +import * as binUtils from '../utils'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; + +class CommandStat extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('stat'); + this.description('Vaults Stat'); + this.argument( + '', + 'Path to where the secret, specified as :', + parsers.parseSecretPath, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (secretPath, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + // Get the secret's stat. + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsStat({ + metadata: auth, + nameOrId: secretPath[0], + secretName: secretPath[1], + }), + meta, + ); + + const data: string[] = [`Stats for "${secretPath[1]}"`]; + for (const [key, value] of Object.entries(response.stat)) { + data.push(`${key}: ${value}`); + } + + // Print out the result. + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandStat; diff --git a/src/secrets/CommandUpdate.ts b/src/secrets/CommandUpdate.ts new file mode 100644 index 00000000..08c57c31 --- /dev/null +++ b/src/secrets/CommandUpdate.ts @@ -0,0 +1,96 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import * as binErrors from '../errors'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; + +class CommandUpdate extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('update'); + this.description('Update a Secret'); + this.argument( + '', + 'On disk path to the secret file with the contents of the updated secret', + ); + this.argument( + '', + 'Path to where the secret to be updated, specified as :', + parsers.parseSecretPath, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (directoryPath, secretPath, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + let content: Buffer; + try { + content = await this.fs.promises.readFile(directoryPath); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsSecretsEdit({ + metadata: auth, + nameOrId: secretPath[0], + secretName: secretPath[1], + secretContent: content.toString('binary'), + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandUpdate; diff --git a/src/secrets/index.ts b/src/secrets/index.ts new file mode 100644 index 00000000..1f4ef08a --- /dev/null +++ b/src/secrets/index.ts @@ -0,0 +1 @@ +export { default } from './CommandSecrets'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..c5406ea3 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,105 @@ +import type { LogLevel } from '@matrixai/logger'; +import type { POJO } from 'polykey/dist/types'; +import type { RecoveryCode } from 'polykey/dist/keys/types'; +import type { Host, Port } from 'polykey/dist/network/types'; +import type { StatusLive } from 'polykey/dist/status/types'; +import type { NodeIdEncoded } from 'polykey/dist/ids/types'; +import type { PrivateKey } from 'polykey/dist/keys/types'; +import type { + PasswordOpsLimit, + PasswordMemLimit, +} from 'polykey/dist/keys/types'; +import type { QUICConfig } from '@matrixai/quic'; + +type AgentStatusLiveData = Omit & { + nodeId: NodeIdEncoded; +}; + +// TODO: fix this... We don't need a dependecy on `@matrixai/quic +type PolykeyQUICConfig = { + // Optionals + keepAliveIntervalTime?: number; + maxIdleTimeout?: number; + // Disabled, set internally + ca?: never; + key?: never; + cert?: never; + verifyPeer?: never; + verifyAllowFail?: never; +} & Partial; + +/** + * PolykeyAgent Starting Input when Backgrounded + * When using advanced serialization, rich structures like + * Map, Set and more can be passed over IPC + * However traditional classes cannot be + */ +type AgentChildProcessInput = { + logLevel: LogLevel; + format: 'human' | 'json'; + workers?: number; + agentConfig: { + password: string; + nodePath?: string; + keyRingConfig?: { + recoveryCode?: RecoveryCode; + privateKey?: PrivateKey; + privateKeyPath?: string; + passwordOpsLimit?: PasswordOpsLimit; + passwordMemLimit?: PasswordMemLimit; + strictMemoryLock?: boolean; + }; + certManagerConfig?: { + certDuration?: number; + }; + nodeConnectionManagerConfig?: { + connConnectTime?: number; + connTimeoutTime?: number; + initialClosestNodes?: number; + pingTimeout?: number; + holePunchTimeout?: number; + holePunchInitialInterval?: number; + }; + networkConfig?: { + // Agent QUICSocket config + agentHost?: Host; + agentPort?: Port; + ipv6Only?: boolean; + // RPCServer for client service + clientHost?: Host; + clientPort?: Port; + // Websocket server config + maxReadableStreamBytes?: number; + connectionIdleTimeoutTime?: number; + pingIntervalTime?: number; + pingTimeoutTime?: number; + // RPC config + clientParserBufferByteLimit?: number; + handlerTimeoutTime?: number; + handlerTimeoutGraceTime?: number; + }; + quicServerConfig?: PolykeyQUICConfig; + quicClientConfig?: PolykeyQUICConfig; + fresh?: boolean; + }; +}; + +/** + * PolykeyAgent starting output when backgrounded + * The error property contains arbitrary error properties + */ +type AgentChildProcessOutput = + | ({ + status: 'SUCCESS'; + recoveryCode?: RecoveryCode; + } & AgentStatusLiveData) + | { + status: 'FAILURE'; + error: POJO; + }; + +export type { + AgentStatusLiveData, + AgentChildProcessInput, + AgentChildProcessOutput, +}; diff --git a/src/utils/ExitHandlers.ts b/src/utils/ExitHandlers.ts new file mode 100644 index 00000000..45999b51 --- /dev/null +++ b/src/utils/ExitHandlers.ts @@ -0,0 +1,170 @@ +import process from 'process'; +import ErrorPolykey from 'polykey/dist/ErrorPolykey'; +import * as binUtils from './utils'; +import * as binErrors from '../errors'; + +class ExitHandlers { + /** + * Mutate this array to control handlers + * Handlers will be executed in reverse order + */ + public handlers: Array<(signal?: NodeJS.Signals) => Promise>; + protected _exiting: boolean = false; + protected _errFormat: 'json' | 'error'; + + /** + * Handles termination signals + * This is idempotent + * After executing handlers, it will re-signal the process group + * This effectively runs the default signal handler in the NodeJS VM + */ + protected signalHandler = async (signal: NodeJS.Signals) => { + if (this._exiting) { + return; + } + this._exiting = true; + try { + await this.executeHandlers(signal); + } catch (e) { + // Due to finally clause, exceptions are caught here + // Signal handling will use signal-based exit codes + // https://nodejs.org/api/process.html#exit-codes + // Therefore `process.exitCode` is not set + if (e instanceof ErrorPolykey) { + process.stderr.write( + binUtils.outputFormatter({ + type: this._errFormat, + data: e, + }), + ); + } else { + // Unknown error, this should not happen + process.stderr.write( + binUtils.outputFormatter({ + type: this._errFormat, + data: e, + }), + ); + } + } finally { + // Uninstall all handlers to prevent signal loop + this.uninstall(); + // Propagate signal to NodeJS VM handlers + process.kill(process.pid, signal); + } + }; + + /** + * Handles asynchronous exceptions + * This prints out appropriate error message on STDERR + * It sets the exit code to SOFTWARE + */ + protected unhandledRejectionHandler = async (e: Error) => { + if (this._exiting) { + return; + } + this._exiting = true; + const error = new binErrors.ErrorBinUnhandledRejection(undefined, { + cause: e, + }); + process.stderr.write( + binUtils.outputFormatter({ + type: this._errFormat, + data: e, + }), + ); + process.exitCode = error.exitCode; + // Fail fast pattern + process.exit(); + }; + + /** + * Handles synchronous exceptions + * This prints out appropriate error message on STDERR + * It sets the exit code to SOFTWARE + */ + protected uncaughtExceptionHandler = async (e: Error) => { + if (this._exiting) { + return; + } + this._exiting = true; + const error = new binErrors.ErrorBinUncaughtException(undefined, { + cause: e, + }); + process.stderr.write( + binUtils.outputFormatter({ + type: this._errFormat, + data: e, + }), + ); + process.exitCode = error.exitCode; + // Fail fast pattern + process.exit(); + }; + + protected deadlockHandler = async () => { + if (process.exitCode == null) { + const e = new binErrors.ErrorBinAsynchronousDeadlock(); + process.stderr.write( + binUtils.outputFormatter({ + type: this._errFormat, + data: e, + }), + ); + process.exitCode = e.exitCode; + } + }; + + /** + * Automatically installs all handlers + */ + public constructor( + handlers: Array<(signal?: NodeJS.Signals) => Promise> = [], + ) { + this.handlers = handlers; + this.install(); + } + + get exiting(): boolean { + return this._exiting; + } + + set errFormat(errFormat: 'json' | 'error') { + this._errFormat = errFormat; + } + + public install() { + process.on('SIGINT', this.signalHandler); + process.on('SIGTERM', this.signalHandler); + process.on('SIGQUIT', this.signalHandler); + process.on('SIGHUP', this.signalHandler); + // Both synchronous and asynchronous errors are handled + process.once('unhandledRejection', this.unhandledRejectionHandler); + process.once('uncaughtException', this.uncaughtExceptionHandler); + process.once('beforeExit', this.deadlockHandler); + } + + public uninstall() { + process.removeListener('SIGINT', this.signalHandler); + process.removeListener('SIGTERM', this.signalHandler); + process.removeListener('SIGQUIT', this.signalHandler); + process.removeListener('SIGHUP', this.signalHandler); + process.removeListener( + 'unhandledRejection', + this.unhandledRejectionHandler, + ); + process.removeListener('uncaughtException', this.uncaughtExceptionHandler); + process.removeListener('beforeExit', this.deadlockHandler); + } + + /** + * Execute handlers in reverse-order to match matroska model + */ + protected async executeHandlers(signal?: NodeJS.Signals) { + for (let i = this.handlers.length - 1, f = this.handlers[i]; i >= 0; i--) { + await f(signal); + } + } +} + +export default ExitHandlers; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..4555c33f --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from './utils'; +export * as options from './options'; +export * as parsers from './parsers'; +export * as processors from './processors'; +export { default as ExitHandlers } from './ExitHandlers'; diff --git a/src/utils/options.ts b/src/utils/options.ts new file mode 100644 index 00000000..9e3570c6 --- /dev/null +++ b/src/utils/options.ts @@ -0,0 +1,227 @@ +/** + * Options and Arguments used by commands + * Use `PolykeyCommand.addOption` + * The option parsers will parse parameters and environment variables + * but not the default value + * @module + */ +import commander from 'commander'; +import config from 'polykey/dist/config'; +import * as binParsers from '../utils/parsers'; + +/** + * Node path is the path to node state + * This is a directory on the filesystem + * This is optional, if it is not specified, we will derive + * platform-specific default node path + * On unknown platforms the default is undefined + */ +const nodePath = new commander.Option( + '-np, --node-path ', + 'Path to Node State', +) + .env('PK_NODE_PATH') + .default(config.defaults.nodePath); + +/** + * Formatting choice of human, json, defaults to human + */ +const format = new commander.Option('-f, --format ', 'Output Format') + .choices(['human', 'json']) + .default('human'); +/** + * Sets log level, defaults to 0, multiple uses will increase verbosity level + */ +const verbose = new commander.Option('-v, --verbose', 'Log Verbose Messages') + .argParser((_, p: number) => { + return p + 1; + }) + .default(0); + +/** + * Ignore any existing state during side-effectual construction + */ +const fresh = new commander.Option( + '--fresh', + 'Ignore existing state during construction', +).default(false); + +/** + * Node ID used for connecting to a remote agent + */ +const nodeId = new commander.Option('-ni, --node-id ') + .env('PK_NODE_ID') + .argParser(binParsers.parseNodeId); + +/** + * Client host used for connecting to remote agent + */ +const clientHost = new commander.Option( + '-ch, --client-host ', + 'Client Host Address', +) + .env('PK_CLIENT_HOST') + .argParser(binParsers.parseHost); + +/** + * Client port used for connecting to remote agent + */ +const clientPort = new commander.Option( + '-cp, --client-port ', + 'Client Port', +) + .env('PK_CLIENT_PORT') + .argParser(binParsers.parsePort); + +const agentHost = new commander.Option('-ah, --agent-host ', 'Agent host') + .env('PK_AGENT_HOST') + .argParser(binParsers.parseHost) + .default(config.defaults.networkConfig.agentHost); + +const agentPort = new commander.Option('-ap, --agent-port ', 'Agent Port') + .env('PK_AGENT_PORT') + .argParser(binParsers.parsePort) + .default(config.defaults.networkConfig.agentPort); + +const connConnectTime = new commander.Option( + '--connection-timeout ', + 'Timeout value for connection establishment between nodes', +) + .argParser(binParsers.parseInteger) + .default(config.defaults.nodeConnectionManagerConfig.connectionConnectTime); + +const passwordFile = new commander.Option( + '-pf, --password-file ', + 'Path to Password', +); + +const passwordNewFile = new commander.Option( + '-pnf, --password-new-file ', + 'Path to new Password', +); + +const recoveryCodeFile = new commander.Option( + '-rcf, --recovery-code-file ', + 'Path to Recovery Code', +); + +const background = new commander.Option( + '-b, --background', + 'Starts the agent as a background process', +); + +const backgroundOutFile = new commander.Option( + '-bof, --background-out-file ', + 'Path to STDOUT for agent process', +); + +const backgroundErrFile = new commander.Option( + '-bef, --background-err-file ', + 'Path to STDERR for agent process', +); + +const seedNodes = new commander.Option( + '-sn, --seed-nodes [nodeId1@host:port;nodeId2@host:port;...]', + 'Seed node address mappings', +) + .argParser(binParsers.parseSeedNodes) + .env('PK_SEED_NODES') + .default([{}, true]); + +const network = new commander.Option( + '-n --network ', + 'Setting the desired default network.', +) + .argParser(binParsers.parseNetwork) + .env('PK_NETWORK') + .default(config.defaults.network.mainnet); + +const workers = new commander.Option( + '-w --workers ', + 'Number of workers to use, defaults to number of cores with `all`, 0 means all cores, `false`|`null`|`none`|`no` means no multi-threading', +) + .argParser(binParsers.parseCoreCount) + .default(0, 'all'); + +const pullVault = new commander.Option( + '-pv, --pull-vault ', + 'Name or Id of the vault to pull from', +); + +const forceNodeAdd = new commander.Option( + '--force', + 'Force adding node to nodeGraph', +).default(false); + +const noPing = new commander.Option('--no-ping', 'Skip ping step').default( + true, +); + +// We can't reference the object here, so we recreate the list of choices +const passwordLimitChoices = [ + 'min', + 'max', + 'interactive', + 'moderate', + 'sensitive', +]; +const passwordOpsLimit = new commander.Option( + '--password-ops-limit ', + 'Limit the password generation operations', +) + .choices(passwordLimitChoices) + .env('PK_PASSWORD_OPS_LIMIT') + .default('moderate'); + +const passwordMemLimit = new commander.Option( + '--password-mem-limit ', + 'Limit the password generation memory', +) + .choices(passwordLimitChoices) + .env('PK_PASSWORD_MEM_LIMIT') + .default('moderate'); + +const privateKeyFile = new commander.Option( + '--private-key-file ', + 'Override key creation with a private key JWE from a file', +); + +const depth = new commander.Option( + '-d, --depth [depth]', + 'The number of commits to retrieve', +).argParser(parseInt); + +const commitId = new commander.Option( + '-ci, --commit-id [commitId]', + 'Id for a specific commit to read from', +); + +export { + nodePath, + format, + verbose, + fresh, + nodeId, + clientHost, + clientPort, + agentHost, + agentPort, + connConnectTime, + recoveryCodeFile, + passwordFile, + passwordNewFile, + background, + backgroundOutFile, + backgroundErrFile, + seedNodes, + network, + workers, + pullVault, + forceNodeAdd, + noPing, + privateKeyFile, + passwordOpsLimit, + passwordMemLimit, + depth, + commitId, +}; diff --git a/src/utils/parsers.ts b/src/utils/parsers.ts new file mode 100644 index 00000000..c968c4af --- /dev/null +++ b/src/utils/parsers.ts @@ -0,0 +1,119 @@ +import commander from 'commander'; +import * as validationUtils from 'polykey/dist/validation/utils'; +import * as validationErrors from 'polykey/dist/validation/errors'; + +/** + * Converts a validation parser to commander argument parser + */ +function validateParserToArgParser( + validate: (data: string) => T, +): (data: string) => T { + return (data: string) => { + try { + return validate(data); + } catch (e) { + if (e instanceof validationErrors.ErrorParse) { + throw new commander.InvalidArgumentError(e.message); + } else { + throw e; + } + } + }; +} + +/** + * Converts a validation parser to commander variadic argument parser. + * Variadic options/arguments are always space-separated. + */ +function validateParserToArgListParser( + validate: (data: string) => T, +): (data: string) => Array { + return (data: string) => { + try { + return data.split(' ').map(validate); + } catch (e) { + if (e instanceof validationErrors.ErrorParse) { + throw new commander.InvalidArgumentError(e.message); + } else { + throw e; + } + } + }; +} + +const parseInteger = validateParserToArgParser(validationUtils.parseInteger); +const parseNumber = validateParserToArgParser(validationUtils.parseNumber); +const parseNodeId = validateParserToArgParser(validationUtils.parseNodeId); +const parseGestaltId = validateParserToArgParser( + validationUtils.parseGestaltId, +); +const parseGestaltAction = validateParserToArgParser( + validationUtils.parseGestaltAction, +); +const parseHost = validateParserToArgParser(validationUtils.parseHost); +const parseHostname = validateParserToArgParser(validationUtils.parseHostname); +const parseHostOrHostname = validateParserToArgParser( + validationUtils.parseHostOrHostname, +); +const parsePort = validateParserToArgParser(validationUtils.parsePort); +const parseNetwork = validateParserToArgParser(validationUtils.parseNetwork); +const parseSeedNodes = validateParserToArgParser( + validationUtils.parseSeedNodes, +); +const parseProviderId = validateParserToArgParser( + validationUtils.parseProviderId, +); +const parseIdentityId = validateParserToArgParser( + validationUtils.parseIdentityId, +); + +const parseProviderIdList = validateParserToArgListParser( + validationUtils.parseProviderId, +); + +function parseCoreCount(v: string): number | undefined { + switch (v) { + case 'all': + return 0; + case 'none': + case 'no': + case 'false': + case 'null': + return undefined; + default: + return parseInt(v); + } +} + +function parseSecretPath(secretPath: string): [string, string, string?] { + // E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned + // If 'vault1:a/b/c=VARIABLE', ['vault1, 'a/b/c', 'VARIABLE'] is returned + const secretPathRegex = + /^([\w-]+)(?::)([\w\-\\\/\.\$]+)(?:=)?([a-zA-Z_][\w]+)?$/; + if (!secretPathRegex.test(secretPath)) { + throw new commander.InvalidArgumentError( + `${secretPath} is not of the format :`, + ); + } + const [, vaultName, directoryPath] = secretPath.match(secretPathRegex)!; + return [vaultName, directoryPath, undefined]; +} + +export { + parseInteger, + parseNumber, + parseNodeId, + parseGestaltId, + parseGestaltAction, + parseHost, + parseHostname, + parseHostOrHostname, + parsePort, + parseNetwork, + parseSeedNodes, + parseProviderId, + parseIdentityId, + parseProviderIdList, + parseCoreCount, + parseSecretPath, +}; diff --git a/src/utils/processors.ts b/src/utils/processors.ts new file mode 100644 index 00000000..8adaf252 --- /dev/null +++ b/src/utils/processors.ts @@ -0,0 +1,421 @@ +import type { FileSystem } from 'polykey/dist/types'; +import type { RecoveryCode } from 'polykey/dist/keys/types'; +import type { NodeId } from 'polykey/dist/ids/types'; +import type { + StatusStarting, + StatusLive, + StatusStopping, + StatusDead, +} from 'polykey/dist/status/types'; +import type { SessionToken } from 'polykey/dist/sessions/types'; +import path from 'path'; +import prompts from 'prompts'; +import Logger from '@matrixai/logger'; +import Status from 'polykey/dist/status/Status'; +import * as clientUtils from 'polykey/dist/client/utils/utils'; +import { arrayZip } from 'polykey/dist/utils'; +import config from 'polykey/dist/config'; +import * as binErrors from '../errors'; + +/** + * Prompts for existing password + * This masks SIGINT handling + * When SIGINT is received this will return undefined + */ +async function promptPassword(): Promise { + const { password } = await prompts({ + name: 'password', + type: 'password', + message: 'Please enter the password', + }); + return password; +} + +/** + * Prompts for new password + * This masks SIGINT handling + * When SIGINT is received this will return undefined + */ +async function promptNewPassword(): Promise { + let password: string | undefined; + while (true) { + ({ password } = await prompts({ + name: 'password', + type: 'password', + message: 'Enter new password', + })); + // If undefined, then SIGINT was sent + // Break the loop and return undefined password + if (password == null) { + break; + } + const { passwordConfirm } = await prompts({ + name: 'passwordConfirm', + type: 'password', + message: 'Confirm new password', + }); + // If undefined, then SIGINT was sent + // Break the loop and return undefined password + if (passwordConfirm == null) { + break; + } + if (password === passwordConfirm) { + break; + } + // Interactive message + process.stderr.write('Passwords do not match!\n'); + } + return password; +} + +/** + * Processes existing password + * Use this when password is necessary + * Order of operations are: + * 1. Reads --password-file + * 2. Reads PK_PASSWORD + * 3. Prompts for password + * This may return an empty string + */ +async function processPassword( + passwordFile?: string, + fs: FileSystem = require('fs'), +): Promise { + let password: string | undefined; + if (passwordFile != null) { + try { + password = (await fs.promises.readFile(passwordFile, 'utf-8')).trim(); + } catch (e) { + throw new binErrors.ErrorCLIPasswordFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + } else if (typeof process.env['PK_PASSWORD'] === 'string') { + password = process.env['PK_PASSWORD']; + } else { + password = await promptPassword(); + if (password === undefined) { + throw new binErrors.ErrorCLIPasswordMissing(); + } + } + return password; +} + +/** + * Processes new password + * Use this when a new password is necessary + * Order of operations are: + * 1. Reads --password-new-file + * 2. Reads PK_PASSWORD_NEW + * 3. Prompts and confirms password + * If processNewPassword is used when an existing password is needed + * for authentication, then the existing boolean should be set to true + * This ensures that this call does not read `PK_PASSWORD` + * This may return an empty string + */ +async function processNewPassword( + passwordNewFile?: string, + fs: FileSystem = require('fs'), + existing: boolean = false, +): Promise { + let passwordNew: string | undefined; + if (passwordNewFile != null) { + try { + passwordNew = ( + await fs.promises.readFile(passwordNewFile, 'utf-8') + ).trim(); + } catch (e) { + throw new binErrors.ErrorCLIPasswordFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + } else if (!existing && typeof process.env['PK_PASSWORD'] === 'string') { + passwordNew = process.env['PK_PASSWORD']; + } else if (typeof process.env['PK_PASSWORD_NEW'] === 'string') { + passwordNew = process.env['PK_PASSWORD_NEW']; + } else { + passwordNew = await promptNewPassword(); + if (passwordNew === undefined) { + throw new binErrors.ErrorCLIPasswordMissing(); + } + } + return passwordNew; +} + +/** + * Process recovery code + * Order of operations are: + * 1. Reads --recovery-code-file + * 2. Reads PK_RECOVERY_CODE + * This may return an empty string + */ +async function processRecoveryCode( + recoveryCodeFile?: string, + fs: FileSystem = require('fs'), +): Promise { + let recoveryCode: string | undefined; + if (recoveryCodeFile != null) { + try { + recoveryCode = ( + await fs.promises.readFile(recoveryCodeFile, 'utf-8') + ).trim(); + } catch (e) { + throw new binErrors.ErrorCLIRecoveryCodeFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + } else if (typeof process.env['PK_RECOVERY_CODE'] === 'string') { + recoveryCode = process.env['PK_RECOVERY_CODE']; + } + return recoveryCode as RecoveryCode | undefined; +} + +/** + * Process client options + * Options are used for connecting PolykeyClient + * Order of operations are: + * 1. Reads --node-id, --client-host, --client-port + * 2. Reads PK_NODE_ID, PK_CLIENT_HOST, PK_CLIENT_PORT + * 3. Command-specific defaults + * 4. If no options are set, reads Status + * Step 2 is done during option construction + * Step 3 is done in CommandPolykey classes + */ +async function processClientOptions( + nodePath: string, + nodeId?: NodeId, + clientHost?: string, + clientPort?: number, + fs = require('fs'), + logger = new Logger(processClientOptions.name), +): Promise<{ + nodeId: NodeId; + clientHost: string; + clientPort: number; +}> { + if (nodeId != null && clientHost != null && clientPort != null) { + return { + nodeId, + clientHost, + clientPort, + }; + } else if (nodeId == null && clientHost == null && clientPort == null) { + const statusPath = path.join(nodePath, config.defaults.statusBase); + const statusLockPath = path.join(nodePath, config.defaults.statusLockBase); + const status = new Status({ + statusPath, + statusLockPath, + fs, + logger: logger.getChild(Status.name), + }); + const statusInfo = await status.readStatus(); + if (statusInfo === undefined || statusInfo.status !== 'LIVE') { + throw new binErrors.ErrorCLIPolykeyAgentStatus('agent is not live'); + } + return { + nodeId: statusInfo.data.nodeId, + clientHost: statusInfo.data.clientHost, + clientPort: statusInfo.data.clientPort, + }; + } else { + const errorMsg = arrayZip( + [nodeId, clientHost, clientPort], + [ + 'missing node ID, provide it with --node-id or PK_NODE_ID', + 'missing client host, provide it with --client-host or PK_CLIENT_HOST', + 'missing client port, provide it with --client-port or PK_CLIENT_PORT', + ], + ) + .flatMap(([option, msg]) => { + if (option == null) { + return [msg]; + } else { + return []; + } + }) + .join('; '); + throw new binErrors.ErrorCLIClientOptions(errorMsg); + } +} + +/** + * Process client status + * Options are used for connecting PolykeyClient + * Variant of processClientOptions + * Use this when you need always need the status info when reading the status + */ +async function processClientStatus( + nodePath: string, + nodeId?: NodeId, + clientHost?: string, + clientPort?: number, + fs = require('fs'), + logger = new Logger(processClientStatus.name), +): Promise< + | { + statusInfo: StatusStarting | StatusStopping | StatusDead; + status: Status; + nodeId: NodeId | undefined; + clientHost: string | undefined; + clientPort: number | undefined; + } + | { + statusInfo: StatusLive; + status: Status; + nodeId: NodeId; + clientHost: string; + clientPort: number; + } + | { + statusInfo: undefined; + status: undefined; + nodeId: NodeId; + clientHost: string; + clientPort: number; + } +> { + if (nodeId != null && clientHost != null && clientPort != null) { + return { + statusInfo: undefined, + status: undefined, + nodeId, + clientHost, + clientPort, + }; + } else if (nodeId == null && clientHost == null && clientPort == null) { + const statusPath = path.join(nodePath, config.defaults.statusBase); + const statusLockPath = path.join(nodePath, config.defaults.statusLockBase); + const status = new Status({ + statusPath, + statusLockPath, + fs, + logger: logger.getChild(Status.name), + }); + const statusInfo = await status.readStatus(); + if (statusInfo == null) { + return { + statusInfo: { status: 'DEAD', data: {} }, + status, + nodeId: undefined, + clientHost: undefined, + clientPort: undefined, + }; + } else if (statusInfo.status === 'LIVE') { + nodeId = statusInfo.data.nodeId; + clientHost = statusInfo.data.clientHost; + clientPort = statusInfo.data.clientPort; + return { + statusInfo, + status, + nodeId, + clientHost, + clientPort, + }; + } else { + return { + statusInfo, + status, + nodeId: undefined, + clientHost: undefined, + clientPort: undefined, + }; + } + } else { + const errorMsg = arrayZip( + [nodeId, clientHost, clientPort], + [ + 'missing node ID, provide it with --node-id or PK_NODE_ID', + 'missing client host, provide it with --client-host or PK_CLIENT_HOST', + 'missing client port, provide it with --client-port or PK_CLIENT_PORT', + ], + ) + .flatMap(([option, msg]) => { + if (option == null) { + return [msg]; + } else { + return []; + } + }) + .join('; '); + throw new binErrors.ErrorCLIClientOptions(errorMsg); + } +} + +/** + * Processes authentication metadata + * Use when authentication is necessary + * Order of operations are: + * 1. Reads --password-file + * 2. Reads PK_PASSWORD + * 3. Reads PK_TOKEN + * 4. Reads Session + * Step 4 is expected to be done during session interception + * This may return an empty metadata + */ +async function processAuthentication( + passwordFile?: string, + fs: FileSystem = require('fs'), +): Promise<{ authorization?: string }> { + if (passwordFile != null) { + let password; + try { + password = (await fs.promises.readFile(passwordFile, 'utf-8')).trim(); + } catch (e) { + throw new binErrors.ErrorCLIPasswordFileRead(e.message, { + data: { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }, + cause: e, + }); + } + return { + authorization: clientUtils.encodeAuthFromPassword(password), + }; + } else if (typeof process.env['PK_PASSWORD'] === 'string') { + return { + authorization: clientUtils.encodeAuthFromPassword( + process.env['PK_PASSWORD'], + ), + }; + } else if (typeof process.env['PK_TOKEN'] === 'string') { + return { + authorization: clientUtils.encodeAuthFromSession( + process.env['PK_TOKEN'] as SessionToken, + ), + }; + } else { + return {}; + } +} + +export { + promptPassword, + promptNewPassword, + processPassword, + processNewPassword, + processRecoveryCode, + processClientOptions, + processClientStatus, + processAuthentication, +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 00000000..9f4613af --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,237 @@ +import type { POJO } from 'polykey/dist/types'; +import process from 'process'; +import { LogLevel } from '@matrixai/logger'; +import ErrorPolykey from 'polykey/dist/ErrorPolykey'; +import * as clientUtils from 'polykey/dist/client/utils/utils'; +import * as clientErrors from 'polykey/dist/client/errors'; +import * as utils from 'polykey/dist/utils'; +import * as rpcErrors from 'polykey/dist/rpc/errors'; +import * as binProcessors from './processors'; +import * as binErrors from '../errors'; + +/** + * Convert verbosity to LogLevel + */ +function verboseToLogLevel(c: number = 0): LogLevel { + let logLevel = LogLevel.WARN; + if (c === 1) { + logLevel = LogLevel.INFO; + } else if (c >= 2) { + logLevel = LogLevel.DEBUG; + } + return logLevel; +} + +type OutputObject = + | { + type: 'raw'; + data: string | Uint8Array; + } + | { + type: 'list'; + data: Array; + } + | { + type: 'table'; + data: Array; + } + | { + type: 'dict'; + data: POJO; + } + | { + type: 'json'; + data: any; + } + | { + type: 'error'; + data: Error; + }; + +function outputFormatter(msg: OutputObject): string | Uint8Array { + let output = ''; + if (msg.type === 'raw') { + return msg.data; + } else if (msg.type === 'list') { + for (let elem in msg.data) { + // Empty string for null or undefined values + if (elem == null) { + elem = ''; + } + output += `${msg.data[elem]}\n`; + } + } else if (msg.type === 'table') { + for (const key in msg.data[0]) { + output += `${key}\t`; + } + output = output.substring(0, output.length - 1) + `\n`; + for (const row of msg.data) { + for (const key in row) { + let value = row[key]; + // Empty string for null or undefined values + if (value == null) { + value = ''; + } + value = value.toString(); + // Remove the last line terminator if it exists + // This may exist if the value is multi-line string + value = value.replace(/(?:\r\n|\n)$/, ''); + output += `${value}\t`; + } + output = output.substring(0, output.length - 1) + `\n`; + } + } else if (msg.type === 'dict') { + for (const key in msg.data) { + let value = msg.data[key]; + // Empty string for null or undefined values + if (value == null) { + value = ''; + } + value = JSON.stringify(value); + // Remove the last line terminator if it exists + // This may exist if the value is multi-line string + value = value.replace(/(?:\r\n|\n)$/, ''); + // If the string has line terminators internally + // Then we must append `\t` separator after each line terminator + value = value.replace(/(\r\n|\n)/g, '$1\t'); + output += `${key}\t${value}\n`; + } + } else if (msg.type === 'json') { + if (msg.data instanceof Error && !(msg.data instanceof ErrorPolykey)) { + msg.data = { + type: msg.data.name, + data: { message: msg.data.message, stack: msg.data.stack }, + }; + } + output = JSON.stringify(msg.data); + output += '\n'; + } else if (msg.type === 'error') { + let currError = msg.data; + let indent = ' '; + while (currError != null) { + if (currError instanceof rpcErrors.ErrorPolykeyRemote) { + output += `${currError.name}: ${currError.description}`; + if (currError.message && currError.message !== '') { + output += ` - ${currError.message}`; + } + if (currError.metadata != null) { + output += '\n'; + for (const [key, value] of Object.entries(currError.metadata)) { + output += `${indent}${key}\t${value}\n`; + } + } + output += `${indent}timestamp\t${currError.timestamp}\n`; + output += `${indent}cause: `; + currError = currError.cause; + } else if (currError instanceof ErrorPolykey) { + output += `${currError.name}: ${currError.description}`; + if (currError.message && currError.message !== '') { + output += ` - ${currError.message}`; + } + output += '\n'; + // Disabled to streamline output + // output += `${indent}exitCode\t${currError.exitCode}\n`; + // output += `${indent}timestamp\t${currError.timestamp}\n`; + if (currError.data && !utils.isEmptyObject(currError.data)) { + output += `${indent}data\t${JSON.stringify(currError.data)}\n`; + } + if (currError.cause) { + output += `${indent}cause: `; + if (currError.cause instanceof ErrorPolykey) { + currError = currError.cause; + } else if (currError.cause instanceof Error) { + output += `${currError.cause.name}`; + if (currError.cause.message && currError.cause.message !== '') { + output += `: ${currError.cause.message}`; + } + output += '\n'; + break; + } else { + output += `${JSON.stringify(currError.cause)}\n`; + break; + } + } else { + break; + } + } else { + output += `${currError.name}`; + if (currError.message && currError.message !== '') { + output += `: ${currError.message}`; + } + output += '\n'; + break; + } + indent = indent + ' '; + } + } + return output; +} + +/** + * CLI Authentication Retry Loop + * Retries unary calls on attended authentication errors + * Known as "privilege elevation" + */ +async function retryAuthentication( + f: (meta: { authorization?: string }) => Promise, + meta: { authorization?: string } = {}, +): Promise { + try { + return await f(meta); + } catch (e) { + // If it is unattended, throw the exception. + // Don't enter into a retry loop when unattended. + // Unattended means that either the `PK_PASSWORD` or `PK_TOKEN` was set. + if ('PK_PASSWORD' in process.env || 'PK_TOKEN' in process.env) { + throw e; + } + // If it is exception is not missing or denied, then throw the exception + const [cause] = remoteErrorCause(e); + if ( + !(cause instanceof clientErrors.ErrorClientAuthMissing) && + !(cause instanceof clientErrors.ErrorClientAuthDenied) + ) { + throw e; + } + } + // Now enter the retry loop + while (true) { + // Prompt the user for password + const password = await binProcessors.promptPassword(); + if (password == null) { + throw new binErrors.ErrorCLIPasswordMissing(); + } + // Augment existing metadata + const auth = { + authorization: clientUtils.encodeAuthFromPassword(password), + }; + try { + return await f(auth); + } catch (e) { + const [cause] = remoteErrorCause(e); + // The auth cannot be missing, so when it is denied do we retry + if (!(cause instanceof clientErrors.ErrorClientAuthDenied)) { + throw e; + } + } + } +} + +function remoteErrorCause(e: any): [any, number] { + let errorCause = e; + let depth = 0; + while (errorCause instanceof rpcErrors.ErrorPolykeyRemote) { + errorCause = errorCause.cause; + depth++; + } + return [errorCause, depth]; +} + +export { + verboseToLogLevel, + outputFormatter, + retryAuthentication, + remoteErrorCause, +}; + +export type { OutputObject }; diff --git a/src/vaults/CommandClone.ts b/src/vaults/CommandClone.ts new file mode 100644 index 00000000..71af46a5 --- /dev/null +++ b/src/vaults/CommandClone.ts @@ -0,0 +1,79 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; + +class CommandClone extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('clone'); + this.description('Clone a Vault from Another Node'); + this.argument('', 'Name or Id of the vault to be cloned'); + this.argument( + '', + 'Id of the node to clone the vault from', + binParsers.parseNodeId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vaultNameOrId, nodeId: NodeId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsClone({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + nameOrId: vaultNameOrId, + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandClone; diff --git a/src/vaults/CommandCreate.ts b/src/vaults/CommandCreate.ts new file mode 100644 index 00000000..b55dcc47 --- /dev/null +++ b/src/vaults/CommandCreate.ts @@ -0,0 +1,77 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandCreate extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('create'); + this.aliases(['touch']); + this.description('Create a new Vault'); + this.argument('', 'Name of the new vault to be created'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vaultName, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const response = await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsCreate({ + metadata: auth, + vaultName: vaultName, + }), + meta, + ); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: [`Vault ${response.vaultIdEncoded} created successfully`], + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandCreate; diff --git a/src/vaults/CommandDelete.ts b/src/vaults/CommandDelete.ts new file mode 100644 index 00000000..ab9d588f --- /dev/null +++ b/src/vaults/CommandDelete.ts @@ -0,0 +1,70 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandDelete extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('delete'); + this.description('Delete an Existing Vault'); + this.argument('', 'Name of the vault to be deleted'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vaultName, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsDelete({ + metadata: auth, + nameOrId: vaultName, + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandDelete; diff --git a/src/vaults/CommandList.ts b/src/vaults/CommandList.ts new file mode 100644 index 00000000..3e087ea2 --- /dev/null +++ b/src/vaults/CommandList.ts @@ -0,0 +1,79 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandList extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('list'); + this.description('List all Available Vaults'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const data = await binUtils.retryAuthentication(async (auth) => { + const data: Array = []; + const stream = await pkClient.rpcClientClient.methods.vaultsList({ + metadata: auth, + }); + for await (const vaultListMessage of stream) { + data.push( + `${vaultListMessage.vaultName}:\t\t${vaultListMessage.vaultIdEncoded}`, + ); + } + return data; + }, meta); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: data, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandList; diff --git a/src/vaults/CommandLog.ts b/src/vaults/CommandLog.ts new file mode 100644 index 00000000..e0ed069f --- /dev/null +++ b/src/vaults/CommandLog.ts @@ -0,0 +1,86 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandLog extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('log'); + this.description('Get the Version History of a Vault'); + this.argument('', 'Name of the vault to obtain the log from'); + this.addOption(binOptions.commitId); + this.addOption(binOptions.depth); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vault, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const data = await binUtils.retryAuthentication(async (auth) => { + const data: Array = []; + const logStream = await pkClient.rpcClientClient.methods.vaultsLog({ + metadata: auth, + nameOrId: vault, + depth: options.depth, + commitId: options.commitId, + }); + for await (const logEntryMessage of logStream) { + data.push(`commit ${logEntryMessage.commitId}`); + data.push(`committer ${logEntryMessage.committer}`); + data.push(`Date: ${logEntryMessage.timestamp}`); + data.push(`${logEntryMessage.message}`); + } + return data; + }, meta); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: data, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandLog; diff --git a/src/vaults/CommandPermissions.ts b/src/vaults/CommandPermissions.ts new file mode 100644 index 00000000..fb4c6447 --- /dev/null +++ b/src/vaults/CommandPermissions.ts @@ -0,0 +1,85 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import * as binProcessors from '../utils/processors'; +import * as binUtils from '../utils'; +import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; + +class CommandPermissions extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('permissions'); + this.alias('perms'); + this.description('Sets the permissions of a vault for Node Ids'); + this.argument('', 'Name or ID of the vault'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vaultName, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const data: Array = []; + await binUtils.retryAuthentication(async (auth) => { + const permissionStream = + await pkClient.rpcClientClient.methods.vaultsPermissionGet({ + metadata: auth, + nameOrId: vaultName, + }); + for await (const permission of permissionStream) { + const nodeId = permission.nodeIdEncoded; + const actions = permission.vaultPermissionList.join(', '); + data.push(`${nodeId}: ${actions}`); + } + return true; + }, meta); + + if (data.length === 0) data.push('No permissions were found'); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: data, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandPermissions; diff --git a/src/vaults/CommandPull.ts b/src/vaults/CommandPull.ts new file mode 100644 index 00000000..fa903f46 --- /dev/null +++ b/src/vaults/CommandPull.ts @@ -0,0 +1,86 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; + +class CommandPull extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('pull'); + this.description('Pull a Vault from Another Node'); + this.argument('', 'Name of the vault to be pulled into'); + this.argument( + '[targetNodeId]', + '(Optional) target node to pull from', + binParsers.parseNodeId, + ); + this.addOption(binOptions.pullVault); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action( + async (vaultNameOrId, targetNodeId: NodeId | undefined, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsPull({ + metadata: auth, + nodeIdEncoded: + targetNodeId != null + ? nodesUtils.encodeNodeId(targetNodeId) + : undefined, + nameOrId: vaultNameOrId, + pullVault: options.pullVault, + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }, + ); + } +} + +export default CommandPull; diff --git a/src/vaults/CommandRename.ts b/src/vaults/CommandRename.ts new file mode 100644 index 00000000..bbc92238 --- /dev/null +++ b/src/vaults/CommandRename.ts @@ -0,0 +1,72 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandRename extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('rename'); + this.description('Rename an Existing Vault'); + this.argument('', 'Name of the vault to be renamed'); + this.argument('', 'New name of the vault'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vaultName, newVaultName, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsRename({ + metadata: auth, + nameOrId: vaultName, + newName: newVaultName, + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandRename; diff --git a/src/vaults/CommandScan.ts b/src/vaults/CommandScan.ts new file mode 100644 index 00000000..cbbeede3 --- /dev/null +++ b/src/vaults/CommandScan.ts @@ -0,0 +1,82 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandScan extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('scan'); + this.description('Scans a node to reveal their shared vaults'); + this.argument('', 'Id of the node to scan'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (nodeId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const data = await binUtils.retryAuthentication(async (auth) => { + const data: Array = []; + const stream = await pkClient.rpcClientClient.methods.vaultsScan({ + metadata: auth, + nodeIdEncoded: nodeId, + }); + for await (const vault of stream) { + const vaultName = vault.vaultName; + const vaultIdEncoded = vault.vaultIdEncoded; + const permissions = vault.permissions.join(','); + data.push(`${vaultName}\t\t${vaultIdEncoded}\t\t${permissions}`); + } + return data; + }, meta); + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'list', + data: data, + }), + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandScan; diff --git a/src/vaults/CommandShare.ts b/src/vaults/CommandShare.ts new file mode 100644 index 00000000..51be8982 --- /dev/null +++ b/src/vaults/CommandShare.ts @@ -0,0 +1,80 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; + +class CommandShare extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('share'); + this.description('Set the Permissions of a Vault for a Node'); + this.argument('', 'Name of the vault to be shared'); + this.argument( + '', + 'Id of the node to share to', + binParsers.parseNodeId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vaultName, nodeId: NodeId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsPermissionSet({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + nameOrId: vaultName, + vaultPermissionList: ['pull', 'clone'], + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandShare; diff --git a/src/vaults/CommandUnshare.ts b/src/vaults/CommandUnshare.ts new file mode 100644 index 00000000..daa01934 --- /dev/null +++ b/src/vaults/CommandUnshare.ts @@ -0,0 +1,80 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import type { NodeId } from 'polykey/dist/ids/types'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binParsers from '../utils/parsers'; + +class CommandUnshare extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('unshare'); + this.description('Unset the Permissions of a Vault for a Node'); + this.argument('', 'Name of the vault to be unshared'); + this.argument( + '', + 'Id of the node to unshare with', + binParsers.parseNodeId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vaultName, nodeId: NodeId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const nodesUtils = await import('polykey/dist/nodes/utils'); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsPermissionUnset({ + metadata: auth, + nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), + nameOrId: vaultName, + vaultPermissionList: ['clone', 'pull'], + }), + meta, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandUnshare; diff --git a/src/vaults/CommandVaults.ts b/src/vaults/CommandVaults.ts new file mode 100644 index 00000000..2c9a5d47 --- /dev/null +++ b/src/vaults/CommandVaults.ts @@ -0,0 +1,35 @@ +import CommandClone from './CommandClone'; +import CommandCreate from './CommandCreate'; +import CommandDelete from './CommandDelete'; +import CommandList from './CommandList'; +import CommandLog from './CommandLog'; +import CommandScan from './CommandScan'; +import CommandPermissions from './CommandPermissions'; +import CommandPull from './CommandPull'; +import CommandRename from './CommandRename'; +import CommandShare from './CommandShare'; +import CommandUnshare from './CommandUnshare'; +import CommandVersion from './CommandVersion'; +import CommandPolykey from '../CommandPolykey'; + +class CommandVaults extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('vaults'); + this.description('Vaults Operations'); + this.addCommand(new CommandClone(...args)); + this.addCommand(new CommandCreate(...args)); + this.addCommand(new CommandDelete(...args)); + this.addCommand(new CommandList(...args)); + this.addCommand(new CommandLog(...args)); + this.addCommand(new CommandPermissions(...args)); + this.addCommand(new CommandPull(...args)); + this.addCommand(new CommandRename(...args)); + this.addCommand(new CommandShare(...args)); + this.addCommand(new CommandUnshare(...args)); + this.addCommand(new CommandVersion(...args)); + this.addCommand(new CommandScan(...args)); + } +} + +export default CommandVaults; diff --git a/src/vaults/CommandVersion.ts b/src/vaults/CommandVersion.ts new file mode 100644 index 00000000..4743d6ea --- /dev/null +++ b/src/vaults/CommandVersion.ts @@ -0,0 +1,79 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type WebSocketClient from 'polykey/dist/websockets/WebSocketClient'; +import CommandPolykey from '../CommandPolykey'; +import * as binUtils from '../utils'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; + +class CommandVersion extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('version'); + this.description('Set a Vault to a Particular Version in its History'); + this.argument('', 'Name of the vault to change the version of'); + this.argument('', 'Id of the commit that will be changed to'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (vault, versionId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const { default: WebSocketClient } = await import( + 'polykey/dist/websockets/WebSocketClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + let webSocketClient: WebSocketClient; + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + if (webSocketClient != null) await webSocketClient.destroy(true); + }); + try { + webSocketClient = await WebSocketClient.createWebSocketClient({ + expectedNodeIds: [clientOptions.nodeId], + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(WebSocketClient.name), + }); + pkClient = await PolykeyClient.createPolykeyClient({ + streamFactory: (ctx) => webSocketClient.startConnection(ctx), + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClientClient.methods.vaultsVersion({ + metadata: auth, + nameOrId: vault, + versionId: versionId, + }), + meta, + ); + /** + * Previous status message: + * --- + * Note: any changes made to the contents of the vault while at this version + * will discard all changes applied to the vault in later versions. You will + * not be able to return to these later versions if changes are made. + */ + } finally { + if (pkClient! != null) await pkClient.stop(); + if (webSocketClient! != null) await webSocketClient.destroy(); + } + }); + } +} + +export default CommandVersion; diff --git a/src/vaults/index.ts b/src/vaults/index.ts new file mode 100644 index 00000000..28bf5618 --- /dev/null +++ b/src/vaults/index.ts @@ -0,0 +1 @@ +export { default } from './CommandVaults'; diff --git a/tests/TestProvider.ts b/tests/TestProvider.ts new file mode 100644 index 00000000..29f1f49d --- /dev/null +++ b/tests/TestProvider.ts @@ -0,0 +1,216 @@ +import type { POJO } from 'polykey/dist/types'; +import type { + ProviderId, + IdentityId, + ProviderToken, + IdentityData, + ProviderAuthenticateRequest, +} from 'polykey/dist/identities/types'; +import type { + IdentitySignedClaim, + ProviderIdentityClaimId, +} from 'polykey/dist/identities/types'; +import type { SignedClaim } from 'polykey/dist/claims/types'; +import type { ClaimLinkIdentity } from 'polykey/dist/claims/payloads'; +import { Provider } from 'polykey/dist/identities'; +import * as identitiesUtils from 'polykey/dist/identities/utils'; +import * as identitiesErrors from 'polykey/dist/identities/errors'; +import * as tokenUtils from 'polykey/dist/tokens/utils'; + +class TestProvider extends Provider { + public readonly id: ProviderId; + + public linkIdCounter: number = 0; + public users: Record; + public links: Record; + protected userLinks: Record>; + protected userTokens: Record; + + public constructor(providerId: ProviderId = 'test-provider' as ProviderId) { + super(); + this.id = providerId; + const testUser = 'test_user' as IdentityId; + this.users = { + [testUser]: { + email: 'test_user@test.com', + connected: ['connected_identity'], + }, + }; + this.userTokens = { + abc123: testUser, + }; + this.links = {}; + this.userLinks = { + [testUser]: ['test_link' as ProviderIdentityClaimId], + }; + } + + public async *authenticate(): AsyncGenerator< + ProviderAuthenticateRequest, + IdentityId + > { + yield { + url: 'test.com', + data: { + userCode: 'randomtestcode', + }, + }; + // Always gives back the abc123 token + const providerToken = { accessToken: 'abc123' }; + const identityId = await this.getIdentityId(providerToken); + await this.putToken(identityId, providerToken); + return identityId; + } + + public async refreshToken(): Promise { + throw new identitiesErrors.ErrorProviderUnimplemented(); + } + + public async getAuthIdentityIds(): Promise> { + const providerTokens = await this.getTokens(); + return Object.keys(providerTokens) as Array; + } + + public async getIdentityId( + providerToken: ProviderToken, + ): Promise { + providerToken = await this.checkToken(providerToken); + return this.userTokens[providerToken.accessToken]; + } + + public async getIdentityData( + authIdentityId: IdentityId, + identityId: IdentityId, + ): Promise { + let providerToken = await this.getToken(authIdentityId); + if (!providerToken) { + throw new identitiesErrors.ErrorProviderUnauthenticated( + `${authIdentityId} has not been authenticated`, + ); + } + providerToken = await this.checkToken(providerToken, authIdentityId); + const user = this.users[identityId]; + if (!user) { + return; + } + return { + providerId: this.id, + identityId: identityId, + name: user.name ?? undefined, + email: user.email ?? undefined, + url: user.url ?? undefined, + }; + } + + public async *getConnectedIdentityDatas( + authIdentityId: IdentityId, + searchTerms: Array = [], + ): AsyncGenerator { + let providerToken = await this.getToken(authIdentityId); + if (!providerToken) { + throw new identitiesErrors.ErrorProviderUnauthenticated( + `${authIdentityId} has not been authenticated`, + ); + } + providerToken = await this.checkToken(providerToken, authIdentityId); + for (const [k, v] of Object.entries(this.users) as Array< + [ + IdentityId, + { name: string; email: string; url: string; connected: Array }, + ] + >) { + if (k === authIdentityId) { + continue; + } + if (!this.users[authIdentityId].connected.includes(k)) { + continue; + } + const data: IdentityData = { + providerId: this.id, + identityId: k, + name: v.name ?? undefined, + email: v.email ?? undefined, + url: v.url ?? undefined, + }; + if (identitiesUtils.matchIdentityData(data, searchTerms)) { + yield data; + } + } + return; + } + + public async publishClaim( + authIdentityId: IdentityId, + identityClaim: SignedClaim, + ): Promise { + const providerToken = await this.getToken(authIdentityId); + if (!providerToken) { + throw new identitiesErrors.ErrorProviderUnauthenticated( + `${authIdentityId} has not been authenticated`, + ); + } + await this.checkToken(providerToken, authIdentityId); + const linkId = this.linkIdCounter.toString() as ProviderIdentityClaimId; + this.linkIdCounter++; + const identityClainEncoded = tokenUtils.generateSignedToken(identityClaim); + this.links[linkId] = JSON.stringify(identityClainEncoded); + this.userLinks[authIdentityId] = this.userLinks[authIdentityId] + ? this.userLinks[authIdentityId] + : []; + const links = this.userLinks[authIdentityId]; + links.push(linkId); + return { + id: linkId, + url: 'test.com', + claim: identityClaim, + }; + } + + public async getClaim( + authIdentityId: IdentityId, + claimId: ProviderIdentityClaimId, + ): Promise { + const providerToken = await this.getToken(authIdentityId); + if (!providerToken) { + throw new identitiesErrors.ErrorProviderUnauthenticated( + `${authIdentityId} has not been authenticated`, + ); + } + await this.checkToken(providerToken, authIdentityId); + const linkClaimData = this.links[claimId]; + if (!linkClaimData) { + return; + } + const linkClaim = this.parseClaim(linkClaimData); + if (!linkClaim) { + return; + } + return { + claim: linkClaim, + id: claimId, + url: 'test.com', + }; + } + + public async *getClaims( + authIdentityId: IdentityId, + identityId: IdentityId, + ): AsyncGenerator { + const providerToken = await this.getToken(authIdentityId); + if (!providerToken) { + throw new identitiesErrors.ErrorProviderUnauthenticated( + `${authIdentityId} has not been authenticated`, + ); + } + await this.checkToken(providerToken, authIdentityId); + const claimIds = this.userLinks[identityId] ?? []; + for (const claimId of claimIds) { + const claimInfo = await this.getClaim(authIdentityId, claimId); + if (claimInfo != null) { + yield claimInfo; + } + } + } +} + +export default TestProvider; diff --git a/tests/agent/lock.test.ts b/tests/agent/lock.test.ts new file mode 100644 index 00000000..05671ef6 --- /dev/null +++ b/tests/agent/lock.test.ts @@ -0,0 +1,81 @@ +import path from 'path'; +import fs from 'fs'; +import prompts from 'prompts'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import Session from 'polykey/dist/sessions/Session'; +import config from 'polykey/dist/config'; +import * as testUtils from '../utils'; + +jest.mock('prompts'); + +describe('lock', () => { + const logger = new Logger('lock test', LogLevel.WARN, [new StreamHandler()]); + let agentDir: string; + let agentPassword: string; + let agentClose: () => Promise; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('lock deletes the session token', async () => { + await testUtils.pkExec(['agent', 'unlock'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }); + const { exitCode } = await testUtils.pkExec(['agent', 'lock'], { + env: { + PK_NODE_PATH: agentDir, + }, + cwd: agentDir, + command: globalThis.testCmd, + }); + expect(exitCode).toBe(0); + const session = await Session.createSession({ + sessionTokenPath: path.join(agentDir, config.defaults.tokenBase), + fs, + logger, + }); + expect(await session.readToken()).toBeUndefined(); + await session.stop(); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'lock ensures re-authentication is required', + async () => { + const password = agentPassword; + prompts.mockClear(); + prompts.mockImplementation(async (_opts: any) => { + return { password }; + }); + await testUtils.pkStdio(['agent', 'unlock'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + }); + // Session token is deleted + await testUtils.pkStdio(['agent', 'lock'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + }); + // Will prompt to reauthenticate + await testUtils.pkStdio(['agent', 'status'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + }); + // Prompted for password 1 time + expect(prompts.mock.calls.length).toBe(1); + prompts.mockClear(); + }, + ); +}); diff --git a/tests/agent/lockall.test.ts b/tests/agent/lockall.test.ts new file mode 100644 index 00000000..e2af008a --- /dev/null +++ b/tests/agent/lockall.test.ts @@ -0,0 +1,126 @@ +import path from 'path'; +import fs from 'fs'; +import prompts from 'prompts'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import Session from 'polykey/dist/sessions/Session'; +import config from 'polykey/dist/config'; +import * as errors from 'polykey/dist/errors'; +import * as testUtils from '../utils'; + +/** + * Mock prompts module which is used prompt for password + */ +jest.mock('prompts'); + +describe('lockall', () => { + const logger = new Logger('lockall test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('lockall deletes the session token', async () => { + await testUtils.pkExec(['agent', 'unlock'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }); + const { exitCode } = await testUtils.pkExec(['agent', 'lockall'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + command: globalThis.testCmd, + }); + expect(exitCode).toBe(0); + const session = await Session.createSession({ + sessionTokenPath: path.join(agentDir, config.defaults.tokenBase), + fs, + logger, + }); + expect(await session.readToken()).toBeUndefined(); + await session.stop(); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'lockall ensures re-authentication is required', + async () => { + const password = agentPassword; + await testUtils.pkStdio(['agent', 'unlock'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + }); + await testUtils.pkStdio(['agent', 'lockall'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + }); + // Token is deleted, re-authentication is required + prompts.mockClear(); + prompts.mockImplementation(async (_opts: any) => { + return { password }; + }); + await testUtils.pkStdio(['agent', 'status'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + }); + // Prompted for password 1 time + expect(prompts.mock.calls.length).toBe(1); + prompts.mockClear(); + }, + ); + testUtils + .testIf(testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker) + .only('lockall causes old session tokens to fail', async () => { + await testUtils.pkExec(['agent', 'unlock'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }); + const session = await Session.createSession({ + sessionTokenPath: path.join(agentDir, config.defaults.tokenBase), + fs, + logger, + }); + const token = await session.readToken(); + await session.stop(); + await testUtils.pkExec(['agent', 'lockall'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }); + // Old token is invalid + const { exitCode, stderr } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_TOKEN: token, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + testUtils.expectProcessError(exitCode, stderr, [ + new errors.ErrorClientAuthDenied(), + ]); + }); +}); diff --git a/tests/agent/start.test.ts b/tests/agent/start.test.ts new file mode 100644 index 00000000..b61876c1 --- /dev/null +++ b/tests/agent/start.test.ts @@ -0,0 +1,1041 @@ +import type { RecoveryCode } from 'polykey/dist/keys/types'; +import type { StatusLive } from 'polykey/dist/status/types'; +import type { NodeId } from 'polykey/dist/ids/types'; +import type { Host, Port } from 'polykey/dist/network/types'; +import path from 'path'; +import fs from 'fs'; +import readline from 'readline'; +import process from 'process'; +import * as jestMockProps from 'jest-mock-props'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import Status from 'polykey/dist/status/Status'; +import * as statusErrors from 'polykey/dist/status/errors'; +import config from 'polykey/dist/config'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import { promise } from 'polykey/dist/utils'; +import * as testUtils from '../utils'; + +describe('start', () => { + const logger = new Logger('start test', LogLevel.WARN, [new StreamHandler()]); + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises + .rm(dataDir, { + force: true, + recursive: true, + }) + // Just ignore failures here + .catch(() => {}); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'start in foreground', + async () => { + const password = 'abc123'; + const polykeyPath = path.join(dataDir, 'polykey'); + await fs.promises.mkdir(polykeyPath); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'polykey'), + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + const rlOut = readline.createInterface(agentProcess.stdout!); + const stdout = await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', () => reject(Error('closed early'))); + }); + const statusLiveData = JSON.parse(stdout); + expect(statusLiveData).toMatchObject({ + pid: expect.any(Number), + nodeId: expect.any(String), + clientHost: expect.any(String), + clientPort: expect.any(Number), + agentHost: expect.any(String), + agentPort: expect.any(Number), + recoveryCode: expect.any(String), + }); + expect( + statusLiveData.recoveryCode.split(' ').length === 12 || + statusLiveData.recoveryCode.split(' ').length === 24, + ).toBe(true); + agentProcess.kill('SIGTERM'); + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const statusInfo = (await status.waitFor('DEAD'))!; + expect(statusInfo.status).toBe('DEAD'); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'start in background', + async () => { + const password = 'abc123'; + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, password); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--password-file', + passwordPath, + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--background', + '--background-out-file', + path.join(dataDir, 'out.log'), + '--background-err-file', + path.join(dataDir, 'err.log'), + '--workers', + 'none', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + const agentProcessExit = new Promise((resolve, reject) => { + agentProcess.on('exit', (code, signal) => { + if (code === 0) { + resolve(); + } else { + reject( + new Error( + `Agent process exited with code: ${code} and signal: ${signal}`, + ), + ); + } + }); + }); + const rlOut = readline.createInterface(agentProcess.stdout!); + const stdout = await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', () => reject(Error('closed early'))); + }); + const statusLiveData = JSON.parse(stdout); + expect(statusLiveData).toMatchObject({ + pid: expect.any(Number), + nodeId: expect.any(String), + clientHost: expect.any(String), + clientPort: expect.any(Number), + agentHost: expect.any(String), + agentPort: expect.any(Number), + recoveryCode: expect.any(String), + }); + // The foreground process PID should nto be the background process PID + expect(statusLiveData.pid).not.toBe(agentProcess.pid); + expect( + statusLiveData.recoveryCode.split(' ').length === 12 || + statusLiveData.recoveryCode.split(' ').length === 24, + ).toBe(true); + await agentProcessExit; + // Make sure that the daemon does output the recovery code + // The recovery code was already written out on agentProcess + const polykeyAgentOut = await fs.promises.readFile( + path.join(dataDir, 'out.log'), + 'utf-8', + ); + expect(polykeyAgentOut).toHaveLength(0); + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const statusInfo1 = (await status.readStatus())!; + expect(statusInfo1).toBeDefined(); + expect(statusInfo1.status).toBe('LIVE'); + process.kill(statusInfo1.data.pid, 'SIGINT'); + // Check for graceful exit + const statusInfo2 = await status.waitFor('DEAD'); + expect(statusInfo2.status).toBe('DEAD'); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'concurrent starts results in 1 success', + async () => { + const password = 'abc123'; + // One of these processes is blocked + const [agentProcess1, agentProcess2] = await Promise.all([ + testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess1'), + ), + testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess2'), + ), + ]); + // These will be the last line of STDERR + // The readline library will automatically trim off newlines + let stdErrLine1; + let stdErrLine2; + const rlErr1 = readline.createInterface(agentProcess1.stderr!); + const rlErr2 = readline.createInterface(agentProcess2.stderr!); + const agentStartedProm1 = promise<[number, string]>(); + const agentStartedProm2 = promise<[number, string]>(); + rlErr1.on('line', (l) => { + stdErrLine1 = l; + if (l.includes('Created PolykeyAgent')) { + agentStartedProm1.resolveP([0, l]); + agentProcess1.kill('SIGINT'); + } + }); + rlErr2.on('line', (l) => { + stdErrLine2 = l; + if (l.includes('Created PolykeyAgent')) { + agentStartedProm2.resolveP([0, l]); + agentProcess2.kill('SIGINT'); + } + }); + + agentProcess1.once('exit', (code) => { + agentStartedProm1.resolveP([code ?? 255, stdErrLine1]); + }); + agentProcess2.once('exit', (code) => { + agentStartedProm2.resolveP([code ?? 255, stdErrLine2]); + }); + + const results = await Promise.all([ + agentStartedProm1.p, + agentStartedProm2.p, + ]); + // Only 1 should fail with locked + const errorStatusLocked = new statusErrors.ErrorStatusLocked(); + let failed = 0; + for (const [code, line] of results) { + if (code !== 0) { + failed += 1; + testUtils.expectProcessError(code, line, [errorStatusLocked]); + } + } + expect(failed).toEqual(1); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'concurrent with bootstrap results in 1 success', + async () => { + const password = 'abc123'; + // One of these processes is blocked + const [agentProcess, bootstrapProcess] = await Promise.all([ + testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess'), + ), + testUtils.pkSpawn( + ['bootstrap', '--fresh', '--verbose', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('bootstrapProcess'), + ), + ]); + // These will be the last line of STDERR + // The readline library will automatically trim off newlines + let stdErrLine1; + let stdErrLine2; + const rlErr1 = readline.createInterface(agentProcess.stderr!); + const rlErr2 = readline.createInterface(bootstrapProcess.stderr!); + const agentStartedProm1 = promise<[number, string]>(); + const agentStartedProm2 = promise<[number, string]>(); + rlErr1.on('line', (l) => { + stdErrLine1 = l; + if (l.includes('Created PolykeyAgent')) { + agentStartedProm1.resolveP([0, l]); + agentProcess.kill('SIGINT'); + } + }); + rlErr2.on('line', (l) => { + stdErrLine2 = l; + if (l.includes('Created PolykeyAgent')) { + agentStartedProm2.resolveP([0, l]); + bootstrapProcess.kill('SIGINT'); + } + }); + + agentProcess.once('exit', (code) => { + agentStartedProm1.resolveP([code ?? 255, stdErrLine1]); + }); + bootstrapProcess.once('exit', (code) => { + agentStartedProm2.resolveP([code ?? 255, stdErrLine2]); + }); + + const results = await Promise.all([ + agentStartedProm1.p, + agentStartedProm2.p, + ]); + // Only 1 should fail with locked + const errorStatusLocked = new statusErrors.ErrorStatusLocked(); + let failed = 0; + for (const [code, line] of results) { + if (code !== 0) { + failed += 1; + testUtils.expectProcessError(code, line, [errorStatusLocked]); + } + } + expect(failed).toEqual(1); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'start with existing state', + async () => { + const password = 'abc123'; + const agentProcess1 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + const rlOut = readline.createInterface(agentProcess1.stdout!); + await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', () => reject(Error('closed early'))); + }); + agentProcess1.kill('SIGHUP'); + const agentProcess2 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + await status.waitFor('LIVE'); + agentProcess2.kill('SIGHUP'); + // Check for graceful exit + const statusInfo = (await status.waitFor('DEAD'))!; + expect(statusInfo.status).toBe('DEAD'); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'start when interrupted, requires fresh on next start', + async () => { + const password = 'password'; + const agentProcess1 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess1'), + ); + const rlErr = readline.createInterface(agentProcess1.stderr!); + // Interrupt when generating the root key pair + await new Promise((resolve, reject) => { + rlErr.once('close', reject); + rlErr.on('line', (l) => { + // This line is brittle + // It may change if the log format changes + // Make sure to keep it updated at the exact point when the DB is created + if (l === 'INFO:polykey.PolykeyAgent.DB:Created DB') { + agentProcess1.kill('SIGINT'); + resolve(); + } + }); + }); + // Unlike bootstrapping, agent start can succeed under certain compatible partial state + // However in some cases, state will conflict, and the start will fail with various errors + // In such cases, the `--fresh` option must be used + const agentProcess2 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--fresh', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess2'), + ); + const rlOut = readline.createInterface(agentProcess2.stdout!); + const stdout = await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', () => reject(Error('closed early'))); + }); + const statusLiveData = JSON.parse(stdout); + expect(statusLiveData).toMatchObject({ + pid: expect.any(Number), + nodeId: expect.any(String), + clientHost: expect.any(String), + clientPort: expect.any(Number), + agentHost: expect.any(String), + agentPort: expect.any(Number), + recoveryCode: expect.any(String), + }); + expect( + statusLiveData.recoveryCode.split(' ').length === 12 || + statusLiveData.recoveryCode.split(' ').length === 24, + ).toBe(true); + agentProcess2.kill('SIGQUIT'); + await testUtils.processExit(agentProcess2); + // Check for graceful exit + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const statusInfo = (await status.readStatus())!; + expect(statusInfo.status).toBe('DEAD'); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'start from recovery code', + async () => { + const password1 = 'abc123'; + const password2 = 'new password'; + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const agentProcess1 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'polykey'), + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password1, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess1'), + ); + const rlOut = readline.createInterface(agentProcess1.stdout!); + const stdout = await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', () => reject(Error('closed early'))); + }); + const statusLiveData = JSON.parse(stdout); + const recoveryCode = statusLiveData.recoveryCode; + const statusInfo1 = (await status.readStatus())!; + agentProcess1.kill('SIGTERM'); + await testUtils.processExit(agentProcess1); + const recoveryCodePath = path.join(dataDir, 'recovery-code'); + await fs.promises.writeFile(recoveryCodePath, recoveryCode + '\n'); + // When recovering, having the wrong bit size is not a problem + const agentProcess2 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--recovery-code-file', + recoveryCodePath, + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password2, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess2'), + ); + const statusInfo2 = await status.waitFor('LIVE'); + expect(statusInfo2.status).toBe('LIVE'); + // Node Id hasn't changed + expect(statusInfo1.data.nodeId).toStrictEqual(statusInfo2.data.nodeId); + agentProcess2.kill('SIGTERM'); + await testUtils.processExit(agentProcess2); + // Check that the password has changed + const agentProcess3 = await testUtils.pkSpawn( + ['agent', 'start', '--workers', 'none', '--verbose'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password2, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess3'), + ); + const statusInfo3 = await status.waitFor('LIVE'); + expect(statusInfo3.status).toBe('LIVE'); + // Node ID hasn't changed + expect(statusInfo1.data.nodeId).toStrictEqual(statusInfo3.data.nodeId); + agentProcess3.kill('SIGTERM'); + await testUtils.processExit(agentProcess3); + // Checks deterministic generation using the same recovery code + // First by deleting the polykey state + await fs.promises.rm(path.join(dataDir, 'polykey'), { + force: true, + recursive: true, + }); + const agentProcess4 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password2, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + PK_RECOVERY_CODE: recoveryCode, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess4'), + ); + const statusInfo4 = await status.waitFor('LIVE'); + expect(statusInfo4.status).toBe('LIVE'); + // Same Node ID as before + expect(statusInfo1.data.nodeId).toStrictEqual(statusInfo4.data.nodeId); + agentProcess4.kill('SIGTERM'); + await testUtils.processExit(agentProcess4); + }, + globalThis.defaultTimeout * 3, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'start with network configuration', + async () => { + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const password = 'abc123'; + // Make sure these ports are not occupied + const clientHost = '127.0.0.2'; + const clientPort = 55555; + const agentHost = '127.0.0.3'; + const agentPort = 55556; + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--workers', + 'none', + '--client-host', + clientHost, + '--client-port', + clientPort.toString(), + '--agent-host', + agentHost, + '--agent-port', + agentPort.toString(), + '--verbose', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('agentProcess'), + ); + const statusInfo = await status.waitFor('LIVE'); + expect(statusInfo.data.clientHost).toBe(clientHost); + expect(statusInfo.data.clientPort).toBe(clientPort); + expect(statusInfo.data.agentHost).toBe(agentHost); + expect(statusInfo.data.agentPort).toBe(agentPort); + agentProcess.kill('SIGTERM'); + // Check for graceful exit + await status.waitFor('DEAD'); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'start with --private-key-file override', + async () => { + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const password = 'abc123'; + const keyPair = keysUtils.generateKeyPair(); + const nodeId = keysUtils.publicKeyToNodeId(keyPair.publicKey); + const privateKeyJWK = keysUtils.privateKeyToJWK(keyPair.privateKey); + const privateKeyJWE = keysUtils.wrapWithPassword( + password, + privateKeyJWK, + keysUtils.passwordOpsLimits.min, + keysUtils.passwordMemLimits.min, + ); + const privateKeyPath = path.join(dataDir, 'private.jwe'); + await fs.promises.writeFile( + privateKeyPath, + JSON.stringify(privateKeyJWE), + { + encoding: 'utf-8', + }, + ); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--workers', + 'none', + '--verbose', + '--private-key-file', + privateKeyPath, + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + const statusInfo = await status.waitFor('LIVE'); + expect(nodeId.equals(statusInfo.data.nodeId)).toBe(true); + agentProcess.kill('SIGINT'); + // Check for graceful exit + await status.waitFor('DEAD'); + }, + globalThis.defaultTimeout * 2, + ); + // TestUtils.describeIf(testUtils.isTestPlatformEmpty) + describe('start with global agent', () => { + let agentDataDir; + let agent1Status: StatusLive; + let agent1Close: () => Promise; + let agent2Status: StatusLive; + let agent2Close: () => Promise; + let seedNodeId1: NodeId; + let seedNodeHost1: string; + let seedNodePort1: number; + let seedNodeId2: NodeId; + let seedNodeHost2: string; + let seedNodePort2: number; + beforeEach(async () => { + // Additional seed node + agentDataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + ({ agentStatus: agent1Status, agentClose: agent1Close } = + await testUtils.setupTestAgent(logger)); + ({ agentStatus: agent2Status, agentClose: agent2Close } = + await testUtils.setupTestAgent(logger)); + seedNodeId1 = agent1Status.data.nodeId; + seedNodeHost1 = agent1Status.data.agentHost; + seedNodePort1 = agent1Status.data.agentPort; + seedNodeId2 = agent2Status.data.nodeId; + seedNodeHost2 = agent2Status.data.agentHost; + seedNodePort2 = agent2Status.data.agentPort; + }); + afterEach(async () => { + await agent1Close(); + await agent2Close(); + await fs.promises.rm(agentDataDir, { + force: true, + recursive: true, + }); + }); + test( + 'start with seed nodes option', + async () => { + const password = 'abc123'; + const nodePath = path.join(dataDir, 'polykey'); + const statusPath = path.join(nodePath, config.defaults.statusBase); + const statusLockPath = path.join( + nodePath, + config.defaults.statusLockBase, + ); + const status = new Status({ + statusPath, + statusLockPath, + fs, + logger, + }); + const mockedConfigDefaultsNetwork = jestMockProps + .spyOnProp(config.defaults, 'network') + .mockValue({ + mainnet: { + [seedNodeId2]: { + host: seedNodeHost2 as Host, + port: seedNodePort2 as Port, + }, + }, + testnet: {}, + }); + await testUtils.pkStdio( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--seed-nodes', + `${seedNodeId1}@${seedNodeHost1}:${seedNodePort1};`, + '--network', + 'mainnet', + '--verbose', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + }, + ); + await testUtils.pkStdio(['agent', 'stop'], { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + }); + mockedConfigDefaultsNetwork.mockRestore(); + await status.waitFor('DEAD'); + }, + globalThis.defaultTimeout * 2, + ); + test( + 'start with seed nodes environment variable', + async () => { + const password = 'abc123'; + const nodePath = path.join(dataDir, 'polykey'); + const statusPath = path.join(nodePath, config.defaults.statusBase); + const statusLockPath = path.join( + nodePath, + config.defaults.statusLockBase, + ); + const status = new Status({ + statusPath, + statusLockPath, + fs, + logger, + }); + const mockedConfigDefaultsNetwork = jestMockProps + .spyOnProp(config.defaults, 'network') + .mockValue({ + mainnet: {}, + testnet: { + [seedNodeId2]: { + host: seedNodeHost2 as Host, + port: seedNodePort2 as Port, + }, + }, + }); + await testUtils.pkStdio( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + PK_SEED_NODES: `;${seedNodeId1}@${seedNodeHost1}:${seedNodePort1}`, + PK_NETWORK: 'testnet', + }, + cwd: dataDir, + }, + ); + await testUtils.pkStdio(['agent', 'stop'], { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + }); + mockedConfigDefaultsNetwork.mockRestore(); + await status.waitFor('DEAD'); + }, + globalThis.defaultTimeout * 2, + ); + }); +}); diff --git a/tests/agent/status.test.ts b/tests/agent/status.test.ts new file mode 100644 index 00000000..b9d9b91b --- /dev/null +++ b/tests/agent/status.test.ts @@ -0,0 +1,238 @@ +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import Status from 'polykey/dist/status/Status'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import config from 'polykey/dist/config'; +import * as testUtils from '../utils'; + +describe('status', () => { + const logger = new Logger('status test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'status on STARTING, STOPPING, DEAD agent', + async () => { + // This test must create its own agent process + const password = 'abc123'; + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + await status.waitFor('STARTING'); + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + // If the command was slow, it may have become LIVE already + expect(JSON.parse(stdout)).toMatchObject({ + status: expect.stringMatching(/STARTING|LIVE/), + pid: expect.any(Number), + }); + await status.waitFor('LIVE'); + const agentProcessExit = testUtils.processExit(agentProcess); + agentProcess.kill('SIGTERM'); + // Cannot wait for STOPPING because waitFor polling may miss the transition + await status.waitFor('DEAD'); + ({ exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + // If the command was slow, it may have become DEAD already + // If it is DEAD, then pid property will be `undefined` + expect(JSON.parse(stdout)).toMatchObject({ + status: expect.stringMatching(/STOPPING|DEAD/), + }); + await agentProcessExit; + ({ exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toMatchObject({ + status: 'DEAD', + }); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('status on missing agent', async () => { + const { exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { PK_NODE_PATH: path.join(dataDir, 'polykey') }, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toMatchObject({ + status: 'DEAD', + }); + }); + describe('status with global agent', () => { + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('status on LIVE agent', async () => { + const status = new Status({ + statusPath: path.join(agentDir, config.defaults.statusBase), + statusLockPath: path.join(agentDir, config.defaults.statusLockBase), + fs, + logger, + }); + const statusInfo = (await status.readStatus())!; + const { exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json', '--verbose'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toMatchObject({ + status: 'LIVE', + pid: expect.any(Number), + nodeId: nodesUtils.encodeNodeId(statusInfo.data.nodeId), + clientHost: statusInfo.data.clientHost, + clientPort: statusInfo.data.clientPort, + agentHost: statusInfo.data.agentHost, + agentPort: statusInfo.data.agentPort, + publicKeyJWK: expect.any(Object), + certChainPEM: expect.any(String), + }); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('status on remote LIVE agent', async () => { + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, agentPassword); + const status = new Status({ + statusPath: path.join(agentDir, config.defaults.statusBase), + statusLockPath: path.join(agentDir, config.defaults.statusLockBase), + fs, + logger, + }); + const statusInfo = (await status.readStatus())!; + // This still needs a `nodePath` because of session token path + const { exitCode, stdout } = await testUtils.pkExec( + [ + 'agent', + 'status', + '--node-path', + dataDir, + '--password-file', + passwordPath, + '--node-id', + nodesUtils.encodeNodeId(statusInfo.data.nodeId), + '--client-host', + statusInfo.data.clientHost, + '--client-port', + statusInfo.data.clientPort.toString(), + '--format', + 'json', + '--verbose', + ], + { + env: {}, + cwd: dataDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toMatchObject({ + status: 'LIVE', + pid: expect.any(Number), + nodeId: nodesUtils.encodeNodeId(statusInfo.data.nodeId), + clientHost: statusInfo.data.clientHost, + clientPort: statusInfo.data.clientPort, + agentHost: statusInfo.data.agentHost, + agentPort: statusInfo.data.agentPort, + publicKeyJWK: expect.any(Object), + certChainPEM: expect.any(String), + }); + }); + }); +}); diff --git a/tests/agent/stop.test.ts b/tests/agent/stop.test.ts new file mode 100644 index 00000000..a7a7c812 --- /dev/null +++ b/tests/agent/stop.test.ts @@ -0,0 +1,312 @@ +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import Status from 'polykey/dist/status/Status'; +import config from 'polykey/dist/config'; +import { sleep } from 'polykey/dist/utils'; +import * as clientErrors from 'polykey/dist/client/errors'; +import * as binErrors from '@/errors'; +import * as testUtils from '../utils'; + +describe('stop', () => { + const logger = new Logger('stop test', LogLevel.WARN, [new StreamHandler()]); + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'stop LIVE agent', + async () => { + const password = 'abc123'; + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + await status.waitFor('LIVE'); + await testUtils.pkExec(['agent', 'stop'], { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }); + await status.waitFor('DEAD'); + await sleep(5000); + agentProcess.kill(); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'stopping is idempotent during concurrent calls and STOPPING or DEAD status', + async () => { + const password = 'abc123'; + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, password); + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + await status.waitFor('LIVE'); + // Simultaneous calls to stop must use pkExec + const [agentStop1, agentStop2] = await Promise.all([ + testUtils.pkExec(['agent', 'stop', '--password-file', passwordPath], { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + }, + cwd: dataDir, + command: globalThis.testCmd, + }), + testUtils.pkExec(['agent', 'stop', '--password-file', passwordPath], { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + }, + cwd: dataDir, + command: globalThis.testCmd, + }), + ]); + // Cannot await for STOPPING + // It's not reliable until file watching is implemented + // So just 1 ms delay until sending another stop command + await sleep(1); + const agentStop3 = await testUtils.pkExec( + ['agent', 'stop', '--node-path', path.join(dataDir, 'polykey')], + { + env: { + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + ); + await status.waitFor('DEAD'); + const agentStop4 = await testUtils.pkExec( + ['agent', 'stop', '--password-file', passwordPath], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + ); + // If the GRPC server gets closed after the GRPC connection is established + // then it's possible that one of these exit codes is 1 + if (agentStop1.exitCode === 1) { + expect(agentStop2.exitCode).toBe(0); + } else if (agentStop2.exitCode === 1) { + expect(agentStop1.exitCode).toBe(0); + } else { + expect(agentStop1.exitCode).toBe(0); + expect(agentStop2.exitCode).toBe(0); + } + expect(agentStop3.exitCode).toBe(0); + expect(agentStop4.exitCode).toBe(0); + agentProcess.kill(); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'stopping starting agent results in error', + async () => { + // This relies on fast execution of `agent stop` while agent is starting, + // docker may not run this fast enough + const password = 'abc123'; + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_FAST_PASSWORD_HASH: 'true', + }, + cwd: dataDir, + }, + logger, + ); + await status.waitFor('STARTING'); + const { exitCode, stderr } = await testUtils.pkStdio( + ['agent', 'stop', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + }, + cwd: dataDir, + }, + ); + testUtils.expectProcessError(exitCode, stderr, [ + new binErrors.ErrorCLIPolykeyAgentStatus('agent is starting'), + ]); + await status.waitFor('LIVE'); + await testUtils.pkStdio(['agent', 'stop'], { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + cwd: dataDir, + }); + await status.waitFor('DEAD'); + agentProcess.kill(); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'stopping while unauthenticated does not stop', + async () => { + const password = 'abc123'; + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + await status.waitFor('LIVE'); + const { exitCode, stderr } = await testUtils.pkExec( + ['agent', 'stop', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: 'wrong password', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + ); + testUtils.expectProcessError(exitCode, stderr, [ + new clientErrors.ErrorClientAuthDenied(), + ]); + // Should still be LIVE + expect((await status.readStatus())?.status).toBe('LIVE'); + await testUtils.pkExec(['agent', 'stop'], { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }); + await status.waitFor('DEAD'); + agentProcess.kill(); + }, + globalThis.defaultTimeout * 2, + ); +}); diff --git a/tests/agent/unlock.test.ts b/tests/agent/unlock.test.ts new file mode 100644 index 00000000..8282bace --- /dev/null +++ b/tests/agent/unlock.test.ts @@ -0,0 +1,72 @@ +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import Session from 'polykey/dist/sessions/Session'; +import config from 'polykey/dist/config'; +import * as testUtils from '../utils'; + +describe('unlock', () => { + const logger = new Logger('unlock test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('unlock acquires session token', async () => { + // Fresh session, to delete the token + const session = await Session.createSession({ + sessionTokenPath: path.join(agentDir, config.defaults.tokenBase), + fs, + logger, + fresh: true, + }); + let exitCode, stdout; + ({ exitCode } = await testUtils.pkExec(['agent', 'unlock'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + })); + expect(exitCode).toBe(0); + // Run command without password + ({ exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toMatchObject({ status: 'LIVE' }); + // Run command with PK_TOKEN + ({ exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_TOKEN: await session.readToken(), + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toMatchObject({ status: 'LIVE' }); + await session.stop(); + }); +}); diff --git a/tests/bootstrap.test.ts b/tests/bootstrap.test.ts new file mode 100644 index 00000000..245efb25 --- /dev/null +++ b/tests/bootstrap.test.ts @@ -0,0 +1,325 @@ +import path from 'path'; +import fs from 'fs'; +import readline from 'readline'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { errors as statusErrors } from 'polykey/dist/status'; +import { errors as bootstrapErrors } from 'polykey/dist/bootstrap'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from './utils'; + +describe('bootstrap', () => { + const logger = new Logger('bootstrap test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'bootstraps node state', + async () => { + const password = 'password'; + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, password); + const { exitCode, stdout } = await testUtils.pkExec( + ['bootstrap', '--password-file', passwordPath, '--verbose'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + const recoveryCode = stdout.trim(); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'bootstraps node state from provided private key', + async () => { + const password = 'password'; + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, password); + const keyPair = keysUtils.generateKeyPair(); + const privateKeyjwK = keysUtils.privateKeyToJWK(keyPair.privateKey); + const privateKeyJWE = keysUtils.wrapWithPassword( + password, + privateKeyjwK, + keysUtils.passwordOpsLimits.min, + keysUtils.passwordMemLimits.min, + ); + const privateKeyPath = path.join(dataDir, 'private.jwe'); + await fs.promises.writeFile( + privateKeyPath, + JSON.stringify(privateKeyJWE), + { + encoding: 'utf-8', + }, + ); + const { exitCode: exitCode1 } = await testUtils.pkExec( + [ + 'bootstrap', + '--password-file', + passwordPath, + '--verbose', + '--private-key-file', + privateKeyPath, + ], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode1).toBe(0); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'bootstrapping occupied node state', + async () => { + const password = 'password'; + await fs.promises.mkdir(path.join(dataDir, 'polykey')); + await fs.promises.writeFile(path.join(dataDir, 'polykey', 'test'), ''); + let exitCode, stdout, stderr; + ({ exitCode, stdout, stderr } = await testUtils.pkExec( + [ + 'bootstrap', + '--node-path', + path.join(dataDir, 'polykey'), + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + const errorBootstrapExistingState = + new bootstrapErrors.ErrorBootstrapExistingState(); + testUtils.expectProcessError(exitCode, stderr, [ + errorBootstrapExistingState, + ]); + ({ exitCode, stdout, stderr } = await testUtils.pkExec( + [ + 'bootstrap', + '--node-path', + path.join(dataDir, 'polykey'), + '--fresh', + '--verbose', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + const recoveryCode = stdout.trim(); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'concurrent bootstrapping results in 1 success', + async () => { + const password = 'password'; + const [bootstrapProcess1, bootstrapProcess2] = await Promise.all([ + testUtils.pkSpawn( + ['bootstrap', '--verbose', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('bootstrapProcess1'), + ), + testUtils.pkSpawn( + ['bootstrap', '--verbose', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('bootstrapProcess2'), + ), + ]); + // These will be the last line of STDERR + // The readline library will automatically trim off newlines + let stdErrLine1; + let stdErrLine2; + const rlErr1 = readline.createInterface(bootstrapProcess1.stderr!); + const rlErr2 = readline.createInterface(bootstrapProcess2.stderr!); + rlErr1.on('line', (l) => { + stdErrLine1 = l; + }); + rlErr2.on('line', (l) => { + stdErrLine2 = l; + }); + const [index, exitCode, signal] = await new Promise< + [number, number | null, NodeJS.Signals | null] + >((resolve) => { + bootstrapProcess1.once('exit', (code, signal) => { + resolve([0, code, signal]); + }); + bootstrapProcess2.once('exit', (code, signal) => { + resolve([1, code, signal]); + }); + }); + const errorStatusLocked = new statusErrors.ErrorStatusLocked(); + expect(signal).toBe(null); + // It's either the first or second process + if (index === 0) { + expect(stdErrLine1).toBeDefined(); + testUtils.expectProcessError(exitCode!, stdErrLine1, [ + errorStatusLocked, + ]); + const [exitCode2] = await testUtils.processExit(bootstrapProcess2); + expect(exitCode2).toBe(0); + } else if (index === 1) { + expect(stdErrLine2).toBeDefined(); + testUtils.expectProcessError(exitCode!, stdErrLine2, [ + errorStatusLocked, + ]); + const [exitCode2] = await testUtils.processExit(bootstrapProcess1); + expect(exitCode2).toBe(0); + } + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'bootstrap when interrupted, requires fresh on next bootstrap', + async () => { + const password = 'password'; + const bootstrapProcess1 = await testUtils.pkSpawn( + ['bootstrap', '--verbose'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger.getChild('bootstrapProcess1'), + ); + const rlErr = readline.createInterface(bootstrapProcess1.stderr!); + // Interrupt when generating the root key pair + await new Promise((resolve, reject) => { + rlErr.once('close', reject); + rlErr.on('line', (l) => { + // This line is brittle + // It may change if the log format changes + // Make sure to keep it updated at the exact point when the root key pair is generated + if ( + l === + 'INFO:polykey.KeyRing:Generating root key pair and recovery code' + ) { + bootstrapProcess1.kill('SIGINT'); + resolve(); + } + }); + }); + await new Promise((res) => { + bootstrapProcess1.once('exit', () => res(null)); + }); + // Attempting to bootstrap should fail with existing state + const bootstrapProcess2 = await testUtils.pkExec( + ['bootstrap', '--verbose', '--format', 'json'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + ); + const errorBootstrapExistingState = + new bootstrapErrors.ErrorBootstrapExistingState(); + testUtils.expectProcessError( + bootstrapProcess2.exitCode, + bootstrapProcess2.stderr, + [errorBootstrapExistingState], + ); + // Attempting to bootstrap with --fresh should succeed + const bootstrapProcess3 = await testUtils.pkExec( + ['bootstrap', '--fresh', '--verbose'], + { + env: { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + ); + expect(bootstrapProcess3.exitCode).toBe(0); + const recoveryCode = bootstrapProcess3.stdout.trim(); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + }, + globalThis.defaultTimeout * 2, + ); +}); diff --git a/tests/global.d.ts b/tests/global.d.ts new file mode 100644 index 00000000..ecd25dd8 --- /dev/null +++ b/tests/global.d.ts @@ -0,0 +1,18 @@ +/* eslint-disable no-var */ + +/// + +/** + * Follows the globals in jest.config.ts + * @module + */ +declare var projectDir: string; +declare var testDir: string; +declare var dataDir: string; +declare var defaultTimeout: number; +declare var polykeyStartupTimeout: number; +declare var failedConnectionTimeout: number; +declare var maxTimeout: number; +declare var testCmd: string | undefined; +declare var testPlatform: string; +declare var tmpDir: string; diff --git a/tests/globalSetup.ts b/tests/globalSetup.ts new file mode 100644 index 00000000..fde41220 --- /dev/null +++ b/tests/globalSetup.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-console */ +import process from 'process'; + +/** + * Global setup for all jest tests + * Side-effects are performed here + * Jest does not support `@/` imports here + */ +async function setup() { + console.log('\nGLOBAL SETUP'); + // The globalDataDir is already created + const globalDataDir = process.env['GLOBAL_DATA_DIR']!; + console.log(`Global Data Dir: ${globalDataDir}`); +} + +export default setup; diff --git a/tests/globalTeardown.ts b/tests/globalTeardown.ts new file mode 100644 index 00000000..0e3e5d30 --- /dev/null +++ b/tests/globalTeardown.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-console */ +import fs from 'fs'; + +/** + * Global teardown for all jest tests + * Side-effects are performed here + * Jest does not support `@/` imports here + */ +async function teardown() { + console.log('GLOBAL TEARDOWN'); + const globalDataDir = process.env['GLOBAL_DATA_DIR']!; + console.log(`Destroying Global Data Dir: ${globalDataDir}`); + await fs.promises.rm(globalDataDir, { recursive: true, force: true }); +} + +export default teardown; diff --git a/tests/identities/allowDisallowPermissions.test.ts b/tests/identities/allowDisallowPermissions.test.ts new file mode 100644 index 00000000..6a46615e --- /dev/null +++ b/tests/identities/allowDisallowPermissions.test.ts @@ -0,0 +1,422 @@ +import type { Host, Port } from 'polykey/dist/network/types'; +import type { IdentityId, ProviderId } from 'polykey/dist/identities/types'; +import type { NodeId } from 'polykey/dist/ids/types'; +import type { ClaimLinkIdentity } from 'polykey/dist/claims/payloads'; +import type { SignedClaim } from 'polykey/dist/claims/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { sysexits } from 'polykey/dist/utils'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as identitiesUtils from 'polykey/dist/identities/utils'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import { encodeProviderIdentityId } from 'polykey/dist/identities/utils'; +import TestProvider from '../TestProvider'; +import * as testUtils from '../utils'; + +// @ts-ignore: stub out method +identitiesUtils.browser = () => {}; + +describe('allow/disallow/permissions', () => { + const logger = new Logger('allow/disallow/permissions test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const password = 'password'; + const provider = new TestProvider(); + const identity = 'abc' as IdentityId; + const providerString = `${provider.id}:${identity}`; + const testToken = { + providerId: 'test-provider' as ProviderId, + identityId: 'test_user' as IdentityId, + }; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let node: PolykeyAgent; + let nodeId: NodeId; + let nodeHost: Host; + let nodePort: Port; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'polykey'); + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + pkAgent.identitiesManager.registerProvider(provider); + // Set up a gestalt to modify the permissions of + const nodePathGestalt = path.join(dataDir, 'gestalt'); + node = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: nodePathGestalt, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + nodeId = node.keyRing.getNodeId(); + nodeHost = node.quicServerAgent.host as unknown as Host; + nodePort = node.quicServerAgent.port as unknown as Port; + node.identitiesManager.registerProvider(provider); + await node.identitiesManager.putToken(provider.id, identity, { + accessToken: 'def456', + }); + provider.users[identity] = {}; + const identityClaim = { + typ: 'ClaimLinkIdentity', + iss: nodesUtils.encodeNodeId(node.keyRing.getNodeId()), + sub: encodeProviderIdentityId([provider.id, identity]), + }; + const [, claim] = await node.sigchain.addClaim(identityClaim); + await provider.publishClaim( + identity, + claim as SignedClaim, + ); + }); + afterEach(async () => { + await node.stop(); + await pkAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'allows/disallows/gets gestalt permissions by node', + async () => { + let exitCode, stdout; + // Add the node to our node graph, otherwise we won't be able to contact it + await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(nodeId), + nodeHost, + `${nodePort}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Must first trust node before we can set permissions + // This is because trusting the node sets it in our gestalt graph, which + // we need in order to set permissions + await testUtils.pkStdio( + ['identities', 'trust', nodesUtils.encodeNodeId(nodeId)], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // We should now have the 'notify' permission, so we'll set the 'scan' + // permission as well + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'allow', nodesUtils.encodeNodeId(nodeId), 'scan'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Check that both permissions are set + ({ exitCode, stdout } = await testUtils.pkStdio( + [ + 'identities', + 'permissions', + nodesUtils.encodeNodeId(nodeId), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + permissions: ['notify', 'scan'], + }); + // Disallow both permissions + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'disallow', nodesUtils.encodeNodeId(nodeId), 'notify'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'disallow', nodesUtils.encodeNodeId(nodeId), 'scan'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Check that both permissions were unset + ({ exitCode, stdout } = await testUtils.pkStdio( + [ + 'identities', + 'permissions', + nodesUtils.encodeNodeId(nodeId), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + permissions: [], + }); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'allows/disallows/gets gestalt permissions by identity', + async () => { + // Can't test with target executable due to mocking + let exitCode, stdout; + // Add the node to our node graph, otherwise we won't be able to contact it + await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(nodeId), + nodeHost, + `${nodePort}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + await testUtils.pkStdio( + [ + 'identities', + 'authenticate', + testToken.providerId, + testToken.identityId, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Must first trust identity before we can set permissions + // This is because trusting the identity sets it in our gestalt graph, + // which we need in order to set permissions + // This command should fail first time since the identity won't be linked + // to any nodes. It will trigger this process via discovery and we must + // wait and then retry + await testUtils.pkStdio(['identities', 'trust', providerString], { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }); + while ((await pkAgent.discovery.waitForDiscoveryTasks()) > 0) { + // Waiting for discovery to complete + } + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'trust', providerString], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // We should now have the 'notify' permission, so we'll set the 'scan' + // permission as well + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'allow', providerString, 'scan'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Check that both permissions are set + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'permissions', providerString, '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + permissions: ['notify', 'scan'], + }); + // Disallow both permissions + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'disallow', providerString, 'notify'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'disallow', providerString, 'scan'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Check that both permissions were unset + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'permissions', providerString, '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + permissions: [], + }); + }, + ); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('should fail on invalid inputs', async () => { + let exitCode; + // Allow + // Invalid gestalt id + ({ exitCode } = await testUtils.pkExec( + ['identities', 'allow', 'invalid', 'notify'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Invalid permission + ({ exitCode } = await testUtils.pkExec( + ['identities', 'allow', nodesUtils.encodeNodeId(nodeId), 'invalid'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Permissions + // Invalid gestalt id + ({ exitCode } = await testUtils.pkExec( + ['identities', 'permissions', 'invalid'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Disallow + // Invalid gestalt id + ({ exitCode } = await testUtils.pkExec( + ['identities', 'disallow', 'invalid', 'notify'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Invalid permission + ({ exitCode } = await testUtils.pkExec( + ['identities', 'disallow', nodesUtils.encodeNodeId(nodeId), 'invalid'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + }); +}); diff --git a/tests/identities/authenticateAuthenticated.test.ts b/tests/identities/authenticateAuthenticated.test.ts new file mode 100644 index 00000000..7590f41c --- /dev/null +++ b/tests/identities/authenticateAuthenticated.test.ts @@ -0,0 +1,155 @@ +import type { IdentityId, ProviderId } from 'polykey/dist/identities/types'; +import type { Host } from 'polykey/dist/network/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { sysexits } from 'polykey/dist/utils'; +import * as identitiesUtils from 'polykey/dist/identities/utils'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import TestProvider from '../TestProvider'; +import * as testUtils from '../utils'; + +// @ts-ignore: stub out method +identitiesUtils.browser = () => {}; + +describe('authenticate/authenticated', () => { + const logger = new Logger('authenticate/authenticated test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const password = 'helloworld'; + const testToken = { + providerId: 'test-provider' as ProviderId, + identityId: 'test_user' as IdentityId, + }; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let testProvider: TestProvider; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'polykey'); + // Cannot use global shared agent since we need to register a provider + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + testProvider = new TestProvider(); + pkAgent.identitiesManager.registerProvider(testProvider); + }); + afterEach(async () => { + await pkAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'authenticates identity with a provider and gets authenticated identity', + async () => { + // Can't test with target command due to mocking + let exitCode, stdout; + // Authenticate an identity + ({ exitCode, stdout } = await testUtils.pkStdio( + [ + 'identities', + 'authenticate', + testToken.providerId, + testToken.identityId, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(stdout).toContain('randomtestcode'); + // Check that the identity was authenticated + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'authenticated', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + providerId: testToken.providerId, + identityId: testToken.identityId, + }); + // Check using providerId flag + ({ exitCode, stdout } = await testUtils.pkStdio( + [ + 'identities', + 'authenticated', + '--provider-id', + testToken.providerId, + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + providerId: testToken.providerId, + identityId: testToken.identityId, + }); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should fail on invalid inputs', + async () => { + let exitCode; + // Authenticate + // Invalid provider + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'authenticate', '', testToken.identityId], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Authenticated + // Invalid provider + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'authenticate', ''], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + }, + ); +}); diff --git a/tests/identities/claim.test.ts b/tests/identities/claim.test.ts new file mode 100644 index 00000000..14808845 --- /dev/null +++ b/tests/identities/claim.test.ts @@ -0,0 +1,156 @@ +import type { + IdentityId, + ProviderId, + ProviderIdentityClaimId, +} from 'polykey/dist/identities/types'; +import type { Host } from 'polykey/dist/network/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { sysexits } from 'polykey/dist/utils'; +import * as identitiesUtils from 'polykey/dist/identities/utils'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import TestProvider from '../TestProvider'; +import * as testUtils from '../utils'; + +// @ts-ignore: stub out method +identitiesUtils.browser = () => {}; + +describe('claim', () => { + const logger = new Logger('claim test', LogLevel.WARN, [new StreamHandler()]); + const password = 'helloworld'; + const testToken = { + providerId: 'test-provider' as ProviderId, + identityId: 'test_user' as IdentityId, + }; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let testProvider: TestProvider; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'polykey'); + // Cannot use global shared agent since we need to register a provider + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + testProvider = new TestProvider(); + pkAgent.identitiesManager.registerProvider(testProvider); + }); + afterEach(async () => { + await pkAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'claims an identity', + async () => { + // Need an authenticated identity + await testUtils.pkStdio( + [ + 'identities', + 'authenticate', + testToken.providerId, + testToken.identityId, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Claim identity + const { exitCode, stdout } = await testUtils.pkStdio( + [ + 'identities', + 'claim', + testToken.providerId, + testToken.identityId, + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual(['Claim Id: 0', 'Url: test.com']); + // Check for claim on the provider + const claim = await testProvider.getClaim( + testToken.identityId, + '0' as ProviderIdentityClaimId, + ); + expect(claim).toBeDefined(); + expect(claim!.id).toBe('0'); + // Expect(claim!.payload.data.type).toBe('identity'); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'cannot claim unauthenticated identities', + async () => { + const { exitCode } = await testUtils.pkStdio( + ['identities', 'claim', testToken.providerId, testToken.identityId], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(sysexits.NOPERM); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should fail on invalid inputs', + async () => { + let exitCode; + // Invalid provider + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'claim', '', testToken.identityId], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Invalid identity + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'claim', testToken.providerId, ''], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + }, + ); +}); diff --git a/tests/identities/discoverGet.test.ts b/tests/identities/discoverGet.test.ts new file mode 100644 index 00000000..64f1a678 --- /dev/null +++ b/tests/identities/discoverGet.test.ts @@ -0,0 +1,317 @@ +import type { IdentityId, ProviderId } from 'polykey/dist/identities/types'; +import type { Host, Port } from 'polykey/dist/network/types'; +import type { NodeId } from 'polykey/dist/ids/types'; +import type { ClaimLinkIdentity } from 'polykey/dist/claims/payloads'; +import type { SignedClaim } from 'polykey/dist/claims/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { sysexits } from 'polykey/dist/utils'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as identitiesUtils from 'polykey/dist/identities/utils'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import { encodeProviderIdentityId } from 'polykey/dist/identities/utils'; +import TestProvider from '../TestProvider'; +import * as testUtils from '../utils'; + +// @ts-ignore: stub out method +identitiesUtils.browser = () => {}; + +describe('discover/get', () => { + const logger = new Logger('discover/get test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const password = 'helloworld'; + const testProvider = new TestProvider(); + const identityId = 'abc' as IdentityId; + const providerString = `${testProvider.id}:${identityId}`; + const testToken = { + providerId: 'test-provider' as ProviderId, + identityId: 'test_user' as IdentityId, + }; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let nodeA: PolykeyAgent; + let nodeB: PolykeyAgent; + let nodeAId: NodeId; + let nodeBId: NodeId; + let nodeAHost: Host; + let nodeAPort: Port; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + // Setup the remote gestalt state here + // Setting up remote nodes + nodeA = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: path.join(dataDir, 'nodeA'), + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + nodeAId = nodeA.keyRing.getNodeId(); + nodeAHost = nodeA.quicServerAgent.host as unknown as Host; + nodeAPort = nodeA.quicServerAgent.port as unknown as Port; + nodeB = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: path.join(dataDir, 'nodeB'), + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + nodeBId = nodeB.keyRing.getNodeId(); + await testUtils.nodesConnect(nodeA, nodeB); + nodePath = path.join(dataDir, 'polykey'); + // Cannot use global shared agent since we need to register a provider + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + pkAgent.identitiesManager.registerProvider(testProvider); + // Add node claim to gestalt + await nodeB.acl.setNodeAction(nodeAId, 'claim'); + await nodeA.nodeManager.claimNode(nodeBId); + // Add identity claim to gestalt + testProvider.users[identityId] = {}; + nodeA.identitiesManager.registerProvider(testProvider); + await nodeA.identitiesManager.putToken(testProvider.id, identityId, { + accessToken: 'abc123', + }); + const identityClaim = { + typ: 'ClaimLinkIdentity', + iss: nodesUtils.encodeNodeId(nodeAId), + sub: encodeProviderIdentityId([testProvider.id, identityId]), + }; + const [, claim] = await nodeA.sigchain.addClaim(identityClaim); + await testProvider.publishClaim( + identityId, + claim as SignedClaim, + ); + }); + afterEach(async () => { + await pkAgent.stop(); + await nodeB.stop(); + await nodeA.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'discovers and gets gestalt by node', + async () => { + await testUtils.pkStdio( + [ + 'identities', + 'authenticate', + testToken.providerId, + testToken.identityId, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Add one of the nodes to our gestalt graph so that we'll be able to + // contact the gestalt during discovery + await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(nodeAId), + nodeAHost, + `${nodeAPort}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Discover gestalt by node + const discoverResponse = await testUtils.pkStdio( + ['identities', 'discover', nodesUtils.encodeNodeId(nodeAId)], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(discoverResponse.exitCode).toBe(0); + // Since discovery is a background process we need to wait for the + while ((await pkAgent.discovery.waitForDiscoveryTasks()) > 0) { + // Gestalt to be discovered + } + // Now we can get the gestalt + const getResponse = await testUtils.pkStdio( + ['identities', 'get', nodesUtils.encodeNodeId(nodeAId)], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(getResponse.exitCode).toBe(0); + expect(getResponse.stdout).toContain(nodesUtils.encodeNodeId(nodeAId)); + expect(getResponse.stdout).toContain(nodesUtils.encodeNodeId(nodeBId)); + expect(getResponse.stdout).toContain(providerString); + // Revert side effects + await pkAgent.gestaltGraph.unsetNode(nodeAId); + await pkAgent.gestaltGraph.unsetNode(nodeBId); + await pkAgent.gestaltGraph.unsetIdentity([testProvider.id, identityId]); + await pkAgent.nodeGraph.unsetNode(nodeAId); + await pkAgent.identitiesManager.delToken( + testToken.providerId, + testToken.identityId, + ); + // @ts-ignore - get protected property + pkAgent.discovery.visitedVertices.clear(); + }, + globalThis.defaultTimeout * 3, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'discovers and gets gestalt by identity', + async () => { + await testUtils.pkStdio( + [ + 'identities', + 'authenticate', + testToken.providerId, + testToken.identityId, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Add one of the nodes to our gestalt graph so that we'll be able to + // contact the gestalt during discovery + await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(nodeAId), + nodeAHost, + `${nodeAPort}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Discover gestalt by node + const discoverResponse = await testUtils.pkStdio( + ['identities', 'discover', providerString], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(discoverResponse.exitCode).toBe(0); + // Since discovery is a background process we need to wait for the + while ((await pkAgent.discovery.waitForDiscoveryTasks()) > 0) { + // Gestalt to be discovered + } + // Now we can get the gestalt + const getResponse = await testUtils.pkStdio( + ['identities', 'get', providerString], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(getResponse.exitCode).toBe(0); + expect(getResponse.stdout).toContain(nodesUtils.encodeNodeId(nodeAId)); + expect(getResponse.stdout).toContain(nodesUtils.encodeNodeId(nodeBId)); + expect(getResponse.stdout).toContain(providerString); + // Revert side effects + await pkAgent.gestaltGraph.unsetNode(nodeAId); + await pkAgent.gestaltGraph.unsetNode(nodeBId); + await pkAgent.gestaltGraph.unsetIdentity([testProvider.id, identityId]); + await pkAgent.nodeGraph.unsetNode(nodeAId); + await pkAgent.identitiesManager.delToken( + testToken.providerId, + testToken.identityId, + ); + // @ts-ignore - get protected property + pkAgent.discovery.visitedVertices.clear(); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should fail on invalid inputs', + async () => { + let exitCode; + // Discover + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'discover', 'invalid'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Get + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'get', 'invalid'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + }, + ); +}); diff --git a/tests/identities/search.test.ts b/tests/identities/search.test.ts new file mode 100644 index 00000000..bb154ae4 --- /dev/null +++ b/tests/identities/search.test.ts @@ -0,0 +1,386 @@ +import type { + IdentityData, + IdentityId, + ProviderId, +} from 'polykey/dist/identities/types'; +import type { Host } from 'polykey/dist/network/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { sysexits } from 'polykey/dist/utils'; +import * as identitiesUtils from 'polykey/dist/identities/utils'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import TestProvider from '../TestProvider'; +import * as testUtils from '../utils'; + +// @ts-ignore: stub out method +identitiesUtils.browser = () => {}; + +describe('search', () => { + const logger = new Logger('search test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const password = 'helloworld'; + const identityId = 'test_user' as IdentityId; + // Provider setup + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const provider3 = new TestProvider('provider3' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + const user2 = { + providerId: provider1.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + const user3 = { + providerId: provider1.id, + identityId: 'user3' as IdentityId, + name: 'User3', + email: 'user3@test.com', + url: 'test.com/user3', + }; + const user4 = { + providerId: provider2.id, + identityId: 'user1' as IdentityId, + name: 'User4', + email: 'user4@test.com', + url: 'test.com/user4', + }; + const user5 = { + providerId: provider2.id, + identityId: 'user2' as IdentityId, + name: 'User5', + email: 'user5@test.com', + url: 'test.com/user5', + }; + const user6 = { + providerId: provider2.id, + identityId: 'user3' as IdentityId, + name: 'User6', + email: 'user6@test.com', + url: 'test.com/user6', + }; + const user7 = { + providerId: provider3.id, + identityId: 'user1' as IdentityId, + name: 'User7', + email: 'user7@test.com', + url: 'test.com/user7', + }; + const user8 = { + providerId: provider3.id, + identityId: 'user2' as IdentityId, + name: 'User8', + email: 'user8@test.com', + url: 'test.com/user8', + }; + const user9 = { + providerId: provider3.id, + identityId: 'user3' as IdentityId, + name: 'User9', + email: 'user9@test.com', + url: 'test.com/user9', + }; + provider1.users['user1'] = user1; + provider1.users['user2'] = user2; + provider1.users['user3'] = user3; + provider2.users['user1'] = user4; + provider2.users['user2'] = user5; + provider2.users['user3'] = user6; + provider3.users['user1'] = user7; + provider3.users['user2'] = user8; + provider3.users['user3'] = user9; + // Connect all identities to our own except for user9 + provider1.users[identityId].connected = [ + user1.identityId, + user2.identityId, + user3.identityId, + ]; + provider2.users[identityId].connected = [ + user4.identityId, + user5.identityId, + user6.identityId, + ]; + provider3.users[identityId].connected = [user7.identityId, user8.identityId]; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'polykey'); + // Cannot use global shared agent since we need to register a provider + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + pkAgent.identitiesManager.registerProvider(provider1); + pkAgent.identitiesManager.registerProvider(provider2); + pkAgent.identitiesManager.registerProvider(provider3); + }); + afterEach(async () => { + await pkAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'finds connected identities', + async () => { + // Can't test with target executable due to mocking + let exitCode, stdout; + let searchResults: Array; + // Search with no authenticated identities + // Should return nothing + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'search', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); + // Authenticate an identity for provider1 + await testUtils.pkStdio( + ['identities', 'authenticate', provider1.id, identityId], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Now our search should include the identities from provider1 + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'search', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + searchResults = stdout.split('\n').slice(undefined, -1).map(JSON.parse); + expect(searchResults).toHaveLength(3); + expect(searchResults).toContainEqual(user1); + expect(searchResults).toContainEqual(user2); + expect(searchResults).toContainEqual(user3); + // Authenticate an identity for provider2 + await testUtils.pkStdio( + ['identities', 'authenticate', provider2.id, identityId], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Now our search should include the identities from provider1 and + // provider2 + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'search', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + searchResults = stdout.split('\n').slice(undefined, -1).map(JSON.parse); + expect(searchResults).toHaveLength(6); + expect(searchResults).toContainEqual(user1); + expect(searchResults).toContainEqual(user2); + expect(searchResults).toContainEqual(user3); + expect(searchResults).toContainEqual(user4); + expect(searchResults).toContainEqual(user5); + expect(searchResults).toContainEqual(user6); + // We can narrow this search by providing search terms + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'search', '4', '5', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + searchResults = stdout.split('\n').slice(undefined, -1).map(JSON.parse); + expect(searchResults).toHaveLength(2); + expect(searchResults).toContainEqual(user4); + expect(searchResults).toContainEqual(user5); + // Authenticate an identity for provider3 + await testUtils.pkStdio( + ['identities', 'authenticate', provider3.id, identityId], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // We can get results from only some providers using the --provider-id + // option + ({ exitCode, stdout } = await testUtils.pkStdio( + [ + 'identities', + 'search', + '--provider-id', + provider2.id, + provider3.id, + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + searchResults = stdout.split('\n').slice(undefined, -1).map(JSON.parse); + expect(searchResults).toHaveLength(5); + expect(searchResults).toContainEqual(user4); + expect(searchResults).toContainEqual(user5); + expect(searchResults).toContainEqual(user6); + expect(searchResults).toContainEqual(user7); + expect(searchResults).toContainEqual(user8); + ({ exitCode, stdout } = await testUtils.pkStdio( + [ + 'identities', + 'search', + '--provider-id', + provider2.id, + '--provider-id', + provider3.id, + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + searchResults = stdout.split('\n').slice(undefined, -1).map(JSON.parse); + expect(searchResults).toHaveLength(5); + expect(searchResults).toContainEqual(user4); + expect(searchResults).toContainEqual(user5); + expect(searchResults).toContainEqual(user6); + expect(searchResults).toContainEqual(user7); + expect(searchResults).toContainEqual(user8); + // We can search for a specific identity id across providers + // This will find identities even if they're disconnected + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'search', '--identity-id', 'user3', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + searchResults = stdout.split('\n').slice(undefined, -1).map(JSON.parse); + expect(searchResults).toHaveLength(3); + expect(searchResults).toContainEqual(user3); + expect(searchResults).toContainEqual(user6); + expect(searchResults).toContainEqual(user9); + // We can limit the number of search results to display + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'search', '--limit', '2', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + searchResults = stdout.split('\n').slice(undefined, -1).map(JSON.parse); + expect(searchResults).toHaveLength(2); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should fail on invalid inputs', + async () => { + let exitCode; + // Invalid identity id + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'search', '--identity-id', ''], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Invalid auth identity id + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'search', '--auth-identity-id', ''], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Invalid value for limit + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'search', '--limit', 'NaN'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + }, + ); +}); diff --git a/tests/identities/trustUntrustList.test.ts b/tests/identities/trustUntrustList.test.ts new file mode 100644 index 00000000..ea551a45 --- /dev/null +++ b/tests/identities/trustUntrustList.test.ts @@ -0,0 +1,409 @@ +import type { Host, Port } from 'polykey/dist/network/types'; +import type { IdentityId, ProviderId } from 'polykey/dist/identities/types'; +import type { NodeId } from 'polykey/dist/ids/types'; +import type { ClaimLinkIdentity } from 'polykey/dist/claims/payloads'; +import type { SignedClaim } from 'polykey/dist/claims/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { sysexits } from 'polykey/dist/utils'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as identitiesUtils from 'polykey/dist/identities/utils'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import { encodeProviderIdentityId } from 'polykey/dist/identities/utils'; +import TestProvider from '../TestProvider'; +import * as testUtils from '../utils'; + +// @ts-ignore: stub out method +identitiesUtils.browser = () => {}; + +describe('trust/untrust/list', () => { + const logger = new Logger('trust/untrust/list test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const password = 'password'; + const identity = 'abc' as IdentityId; + const testToken = { + providerId: 'test-provider' as ProviderId, + identityId: 'test_user' as IdentityId, + }; + let provider: TestProvider; + let providerString: string; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let node: PolykeyAgent; + let nodeId: NodeId; + let nodeHost: Host; + let nodePort: Port; + beforeEach(async () => { + provider = new TestProvider(); + providerString = `${provider.id}:${identity}`; + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'polykey'); + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + pkAgent.identitiesManager.registerProvider(provider); + // Set up a gestalt to trust + const nodePathGestalt = path.join(dataDir, 'gestalt'); + node = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: nodePathGestalt, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + nodeId = node.keyRing.getNodeId(); + nodeHost = node.quicServerAgent.host as unknown as Host; + nodePort = node.quicServerAgent.port as unknown as Port; + node.identitiesManager.registerProvider(provider); + await node.identitiesManager.putToken(provider.id, identity, { + accessToken: 'def456', + }); + provider.users[identity] = {}; + const identityClaim = { + typ: 'ClaimLinkIdentity', + iss: nodesUtils.encodeNodeId(node.keyRing.getNodeId()), + sub: encodeProviderIdentityId([provider.id, identity]), + }; + const [, claim] = await node.sigchain.addClaim(identityClaim); + await provider.publishClaim( + identity, + claim as SignedClaim, + ); + }); + afterEach(async () => { + await node.stop(); + await pkAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'trusts and untrusts a gestalt by node, adds it to the gestalt graph, and lists the gestalt with notify permission', + async () => { + let exitCode, stdout; + // Add the node to our node graph and authenticate an identity on the + // provider + // This allows us to contact the members of the gestalt we want to trust + await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(nodeId), + nodeHost, + `${nodePort}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + await testUtils.pkStdio( + [ + 'identities', + 'authenticate', + testToken.providerId, + testToken.identityId, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Trust node - this should trigger discovery on the gestalt the node + // belongs to and add it to our gestalt graph + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'trust', nodesUtils.encodeNodeId(nodeId)], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Since discovery is a background process we need to wait for the + // gestalt to be discovered + let existingTasks: number = 0; + do { + existingTasks = await pkAgent.discovery.waitForDiscoveryTasks(); + } while (existingTasks > 0); + // Check that gestalt was discovered and permission was set + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'list', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toHaveLength(2); + expect(JSON.parse(stdout)[0]).toEqual({ + permissions: ['notify'], + nodes: [{ nodeId: nodesUtils.encodeNodeId(nodeId) }], + identities: [ + { + providerId: provider.id, + identityId: identity, + }, + ], + }); + // Untrust the gestalt by node + // This should remove the permission, but not the gestalt (from the gestalt + // graph) + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'untrust', nodesUtils.encodeNodeId(nodeId)], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Check that gestalt still exists but has no permissions + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'list', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toHaveLength(2); + expect(JSON.parse(stdout)[0]).toEqual({ + permissions: null, + nodes: [{ nodeId: nodesUtils.encodeNodeId(nodeId) }], + identities: [ + { + providerId: provider.id, + identityId: identity, + }, + ], + }); + // Revert side-effects + await pkAgent.gestaltGraph.unsetNode(nodeId); + await pkAgent.gestaltGraph.unsetIdentity([provider.id, identity]); + await pkAgent.nodeGraph.unsetNode(nodeId); + await pkAgent.identitiesManager.delToken( + testToken.providerId, + testToken.identityId, + ); + // @ts-ignore - get protected property + pkAgent.discovery.visitedVertices.clear(); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'trusts and untrusts a gestalt by identity, adds it to the gestalt graph, and lists the gestalt with notify permission', + async () => { + let exitCode, stdout; + // Add the node to our node graph and authenticate an identity on the + // provider + // This allows us to contact the members of the gestalt we want to trust + await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(nodeId), + nodeHost, + `${nodePort}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + await testUtils.pkStdio( + [ + 'identities', + 'authenticate', + testToken.providerId, + testToken.identityId, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + // Trust identity - this should trigger discovery on the gestalt the node + // belongs to and add it to our gestalt graph + // This command should fail first time as we need to allow time for the + // identity to be linked to a node in the node graph + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'trust', providerString], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.NOUSER); + // Since discovery is a background process we need to wait for the + // gestalt to be discovered + let existingTasks: number = 0; + do { + existingTasks = await pkAgent.discovery.waitForDiscoveryTasks(); + } while (existingTasks > 0); + // This time the command should succeed + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'trust', providerString], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Check that gestalt was discovered and permission was set + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'list', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toHaveLength(2); + expect(JSON.parse(stdout)[0]).toEqual({ + permissions: ['notify'], + nodes: [{ nodeId: nodesUtils.encodeNodeId(nodeId) }], + identities: [ + { + providerId: provider.id, + identityId: identity, + }, + ], + }); + // Untrust the gestalt by node + // This should remove the permission, but not the gestalt (from the gestalt + // graph) + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'untrust', providerString], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Check that gestalt still exists but has no permissions + ({ exitCode, stdout } = await testUtils.pkStdio( + ['identities', 'list', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toHaveLength(2); + expect(JSON.parse(stdout)[0]).toEqual({ + permissions: null, + nodes: [{ nodeId: nodesUtils.encodeNodeId(nodeId) }], + identities: [ + { + providerId: provider.id, + identityId: identity, + }, + ], + }); + // Revert side-effects + await pkAgent.gestaltGraph.unsetNode(nodeId); + await pkAgent.gestaltGraph.unsetIdentity([provider.id, identity]); + await pkAgent.nodeGraph.unsetNode(nodeId); + await pkAgent.identitiesManager.delToken( + testToken.providerId, + testToken.identityId, + ); + // @ts-ignore - get protected property + pkAgent.discovery.visitedVertices.clear(); + }, + globalThis.defaultTimeout * 2, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should fail on invalid inputs', + async () => { + let exitCode; + // Trust + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'trust', 'invalid'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + // Untrust + ({ exitCode } = await testUtils.pkStdio( + ['identities', 'untrust', 'invalid'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(sysexits.USAGE); + }, + ); +}); diff --git a/tests/integration/testnet/testnetConnection.test.ts b/tests/integration/testnet/testnetConnection.test.ts new file mode 100644 index 00000000..2842ad74 --- /dev/null +++ b/tests/integration/testnet/testnetConnection.test.ts @@ -0,0 +1,304 @@ +import type { NodeIdEncoded, SeedNodes } from 'polykey/dist/nodes/types'; +import path from 'path'; +import fs from 'fs'; +import readline from 'readline'; +import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import config from 'polykey/dist/config'; +import { sleep } from 'polykey/dist/utils'; +import * as testUtils from '../../utils'; + +test('dummy test', async () => {}); + +describe.skip('testnet connection', () => { + const logger = new Logger('TCT', LogLevel.WARN, [new StreamHandler()]); + const format = formatting.format`${formatting.keys}:${formatting.msg}`; + logger.handlers.forEach((handler) => handler.setFormatter(format)); + const seedNodes = Object.entries(config.defaults.network.testnet); + const seedNodeId1 = seedNodes[0][0] as NodeIdEncoded; + const seedNodeAddress1 = seedNodes[0][1]; + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test('can connect to `testnet.polykey.io` seed node', async () => { + const password = 'abc123'; + const nodePath = path.join(dataDir, 'polykey'); + // Starting an agent with the testnet as a seed node + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--seed-nodes', + `${seedNodeId1}@${seedNodeAddress1.host}:${seedNodeAddress1.port}`, + '--format', + 'json', + '--verbose', + '--workers', + '0', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + logger, + ); + try { + const rlOut = readline.createInterface(agentProcess.stdout!); + await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }); + + // Pinging the seed node + const { exitCode: exitCode1 } = await testUtils.pkStdio( + ['nodes', 'ping', seedNodeId1, '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode1).toBe(0); + } finally { + agentProcess.kill('SIGINT'); + await testUtils.processExit(agentProcess); + } + }); + test('network expands when connecting to seed node', async () => { + const password = 'abc123'; + // Starting two nodes with the testnet as the seed node + const nodePathA = path.join(dataDir, 'polykeyA'); + const nodePathB = path.join(dataDir, 'polykeyB'); + const agentProcessA = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--seed-nodes', + `${seedNodeId1}@${seedNodeAddress1.host}:${seedNodeAddress1.port}`, + '--format', + 'json', + '--verbose', + '--workers', + '0', + ], + { + env: { + PK_NODE_PATH: nodePathA, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + logger, + ); + const agentProcessB = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--seed-nodes', + `${seedNodeId1}@${seedNodeAddress1.host}:${seedNodeAddress1.port}`, + '--format', + 'json', + '--verbose', + '--workers', + '0', + ], + { + env: { + PK_NODE_PATH: nodePathB, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + logger, + ); + + try { + const rlOutA = readline.createInterface(agentProcessA.stdout!); + const stdoutA = await new Promise((resolve, reject) => { + rlOutA.once('line', resolve); + rlOutA.once('close', reject); + }); + const statusLiveDataA = JSON.parse(stdoutA); + const nodeIdA = statusLiveDataA.nodeId; + + const rlOutB = readline.createInterface(agentProcessB.stdout!); + const stdoutB = await new Promise((resolve, reject) => { + rlOutB.once('line', resolve); + rlOutB.once('close', reject); + }); + const statusLiveDataB = JSON.parse(stdoutB); + const nodeIdB = statusLiveDataB.nodeId; + + // NodeA should ping the seed node + const { exitCode: exitCode1 } = await testUtils.pkStdio( + ['nodes', 'ping', seedNodeId1, '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePathA, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode1).toBe(0); + + // NodeB should ping the seed node + const { exitCode: exitCode2 } = await testUtils.pkStdio( + ['nodes', 'ping', seedNodeId1, '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePathB, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode2).toBe(0); + + // NodeA should be able to ping to NodeB + const { exitCode: exitCode3 } = await testUtils.pkStdio( + ['nodes', 'ping', nodeIdB, '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePathA, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode3).toBe(0); + + // NodeB should be able to ping to NodeA + const { exitCode: exitCode4 } = await testUtils.pkStdio( + ['nodes', 'ping', nodeIdA, '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePathB, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode4).toBe(0); + } finally { + agentProcessA.kill('SIGINT'); + agentProcessB.kill('SIGINT'); + await testUtils.processExit(agentProcessA); + await testUtils.processExit(agentProcessB); + } + }); + // This test is known to fail, two nodes on the same network can't hole punch + test.skip('testing hole punching', async () => { + // Const nodePathS = path.join(dataDir, 'seed'); + const nodePath1 = path.join(dataDir, 'node1'); + const nodePath2 = path.join(dataDir, 'node2'); + const password = 'password'; + const localhost = '127.0.0.1'; + logger.setLevel(LogLevel.WARN); + // Console.log('Starting Seed'); + // const seed = await PolykeyAgent.createPolykeyAgent({ + // password, + // nodePath: nodePathS, + // networkConfig: { + // proxyHost: localhost, + // agentHost: localhost, + // clientHost: localhost, + // forwardHost: localhost, + // proxyPort: 55550, + // }, + // keysConfig: { + // privateKeyPemOverride: globalRootKeyPems[0], + // }, + // logger: logger.getChild('S'), + // }); + // const seedNodes: SeedNodes = { + // [nodesUtils.encodeNodeId(seed.keyManager.getNodeId())]: { + // host: seed.proxy.getProxyHost(), + // port: seed.proxy.getProxyPort(), + // }, + // }; + const seedNodes: SeedNodes = { + [seedNodeId1]: seedNodeAddress1, + }; + // Console.log('Starting Agent1'); + const agent1 = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: nodePath1, + seedNodes, + networkConfig: { + // ProxyHost: localhost, + agentHost: localhost, + clientHost: localhost, + agentPort: 55551, + }, + + logger: logger.getChild('A1'), + }); + // Console.log('Starting Agent2'); + logger.setLevel(LogLevel.WARN); + const agent2 = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: nodePath2, + seedNodes, + networkConfig: { + agentHost: localhost, + clientHost: localhost, + agentPort: 55552, + }, + logger: logger.getChild('A2'), + }); + + try { + logger.setLevel(LogLevel.WARN); + // Console.log('syncing 1'); + // await agent1.nodeManager.syncNodeGraph(true); + // console.log('syncing 2'); + // await agent2.nodeManager.syncNodeGraph(true); + + // seed hun0 + // agent1 ijtg + // agent2 fhmg + + // Ping the node + await sleep(5000); + // Console.log( + // nodesUtils.encodeNodeId(agent1.keyManager.getNodeId()), + // agent1.proxy.getProxyHost(), + // agent1.proxy.getProxyPort(), + // ); + // console.log( + // nodesUtils.encodeNodeId(agent2.keyManager.getNodeId()), + // agent2.proxy.getProxyHost(), + // agent2.proxy.getProxyPort(), + // ); + // console.log('Attempting ping'); + const pingResult = await agent2.nodeManager.pingNode( + agent1.keyRing.getNodeId(), + ); + // Console.log(pingResult); + expect(pingResult).toBe(true); + } finally { + logger.setLevel(LogLevel.WARN); + // Console.log('cleaning up'); + // Await seed.stop(); + await agent1.stop(); + await agent2.stop(); + } + }); + + // We want to ping each other +}); diff --git a/tests/keys/cert.test.ts b/tests/keys/cert.test.ts new file mode 100644 index 00000000..d769a72d --- /dev/null +++ b/tests/keys/cert.test.ts @@ -0,0 +1,51 @@ +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from '../utils'; + +describe('cert', () => { + const logger = new Logger('cert test', LogLevel.WARN, [new StreamHandler()]); + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('cert gets the certificate', async () => { + let { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'cert', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + cert: expect.any(String), + }); + const certCommand = JSON.parse(stdout).cert; + ({ exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + const certStatus = JSON.parse(stdout).certChainPEM; + expect(certCommand).toBe(certStatus); + }); +}); diff --git a/tests/keys/certchain.test.ts b/tests/keys/certchain.test.ts new file mode 100644 index 00000000..619201a6 --- /dev/null +++ b/tests/keys/certchain.test.ts @@ -0,0 +1,53 @@ +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from '../utils'; + +describe('certchain', () => { + const logger = new Logger('certchain test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('certchain gets the certificate chain', async () => { + let { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'certchain', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + certchain: expect.any(Array), + }); + const certChainCommand = JSON.parse(stdout).certchain.join('\n'); + ({ exitCode, stdout } = await testUtils.pkExec( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + const certChainStatus = JSON.parse(stdout).rootCertChainPem; + expect(certChainCommand.rootPublicKeyPem).toBe(certChainStatus); + }); +}); diff --git a/tests/keys/encryptDecrypt.test.ts b/tests/keys/encryptDecrypt.test.ts new file mode 100644 index 00000000..e66d3f57 --- /dev/null +++ b/tests/keys/encryptDecrypt.test.ts @@ -0,0 +1,147 @@ +import type { StatusLive } from 'polykey/dist/status/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import sysexits from 'polykey/dist/utils/sysexits'; +import * as testUtils from '../utils'; + +describe('encrypt-decrypt', () => { + const logger = new Logger('encrypt-decrypt test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + let agentStatus: StatusLive; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose, agentStatus } = + await testUtils.setupTestAgent(logger)); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('decrypts data', async () => { + const dataPath = path.join(agentDir, 'data'); + const publicKey = keysUtils.publicKeyFromNodeId(agentStatus.data.nodeId); + const encrypted = keysUtils.encryptWithPublicKey( + publicKey, + Buffer.from('abc'), + ); + await fs.promises.writeFile(dataPath, encrypted, { + encoding: 'binary', + }); + const { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'decrypt', dataPath, '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + decryptedData: 'abc', + }); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('encrypts data using NodeId', async () => { + const targetkeyPair = keysUtils.generateKeyPair(); + const targetNodeId = keysUtils.publicKeyToNodeId(targetkeyPair.publicKey); + + const dataPath = path.join(agentDir, 'data'); + await fs.promises.writeFile(dataPath, 'abc', { + encoding: 'binary', + }); + const { exitCode, stdout } = await testUtils.pkExec( + [ + 'keys', + 'encrypt', + dataPath, + nodesUtils.encodeNodeId(targetNodeId), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + encryptedData: expect.any(String), + }); + const encrypted = JSON.parse(stdout).encryptedData; + const decrypted = keysUtils.decryptWithPrivateKey( + targetkeyPair, + Buffer.from(encrypted, 'binary'), + ); + expect(decrypted?.toString()).toBe('abc'); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('encrypts data using JWK file', async () => { + const targetkeyPair = keysUtils.generateKeyPair(); + const publicJWK = keysUtils.publicKeyToJWK(targetkeyPair.publicKey); + + const dataPath = path.join(agentDir, 'data'); + const jwkPath = path.join(agentDir, 'jwk'); + await fs.promises.writeFile(jwkPath, JSON.stringify(publicJWK), 'utf-8'); + await fs.promises.writeFile(dataPath, 'abc', { + encoding: 'binary', + }); + const { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'encrypt', dataPath, jwkPath, '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + encryptedData: expect.any(String), + }); + const encrypted = JSON.parse(stdout).encryptedData; + const decrypted = keysUtils.decryptWithPrivateKey( + targetkeyPair, + Buffer.from(encrypted, 'binary'), + ); + expect(decrypted?.toString()).toBe('abc'); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('encrypts data fails with invalid JWK file', async () => { + const dataPath = path.join(agentDir, 'data'); + const jwkPath = path.join(agentDir, 'jwk'); + await fs.promises.writeFile(dataPath, 'abc', { + encoding: 'binary', + }); + const { exitCode } = await testUtils.pkExec( + ['keys', 'encrypt', dataPath, jwkPath, '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(sysexits.NOINPUT); + }); +}); diff --git a/tests/keys/keypair.test.ts b/tests/keys/keypair.test.ts new file mode 100644 index 00000000..47cd2a6f --- /dev/null +++ b/tests/keys/keypair.test.ts @@ -0,0 +1,52 @@ +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from '../utils'; + +describe('keypair', () => { + const logger = new Logger('keypair test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('keypair gets private and public key', async () => { + const { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'keypair', 'password', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + PK_PASSWORD_NEW: 'newPassword', + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + publicKey: { + alg: expect.any(String), + crv: expect.any(String), + ext: expect.any(Boolean), + key_ops: expect.any(Array), + kty: expect.any(String), + x: expect.any(String), + }, + privateKey: { + ciphertext: expect.any(String), + iv: expect.any(String), + protected: expect.any(String), + tag: expect.any(String), + }, + }); + }); +}); diff --git a/tests/keys/password.test.ts b/tests/keys/password.test.ts new file mode 100644 index 00000000..aea17e6d --- /dev/null +++ b/tests/keys/password.test.ts @@ -0,0 +1,50 @@ +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from '../utils'; + +describe('password', () => { + const logger = new Logger('password test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('password changes the root password', async () => { + const passPath = path.join(agentDir, 'passwordChange'); + await fs.promises.writeFile(passPath, 'password-change'); + let { exitCode } = await testUtils.pkExec( + ['keys', 'password', '--password-new-file', passPath], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + // Old password should no longer work + ({ exitCode } = await testUtils.pkExec(['keys', 'keypair'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + PK_PASSWORD_NEW: 'newPassword2', + }, + cwd: agentDir, + command: globalThis.testCmd, + })); + expect(exitCode).toBe(77); + }); +}); diff --git a/tests/keys/private.test.ts b/tests/keys/private.test.ts new file mode 100644 index 00000000..50abd496 --- /dev/null +++ b/tests/keys/private.test.ts @@ -0,0 +1,42 @@ +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from '../utils'; + +describe('private', () => { + const logger = new Logger('private test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('private gets private key', async () => { + const { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'private', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + PK_PASSWORD_NEW: 'newPassword', + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + ciphertext: expect.any(String), + iv: expect.any(String), + protected: expect.any(String), + tag: expect.any(String), + }); + }); +}); diff --git a/tests/keys/public.test.ts b/tests/keys/public.test.ts new file mode 100644 index 00000000..289d5a18 --- /dev/null +++ b/tests/keys/public.test.ts @@ -0,0 +1,43 @@ +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from '../utils'; + +describe('public', () => { + const logger = new Logger('public test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('public gets public key', async () => { + const { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'public', 'password', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + alg: expect.any(String), + crv: expect.any(String), + ext: expect.any(Boolean), + key_ops: expect.any(Array), + kty: expect.any(String), + x: expect.any(String), + }); + }); +}); diff --git a/tests/keys/renew.test.ts b/tests/keys/renew.test.ts new file mode 100644 index 00000000..78bfc6c9 --- /dev/null +++ b/tests/keys/renew.test.ts @@ -0,0 +1,133 @@ +import type { Host } from 'polykey/dist/network/types'; +import type { NodeId } from 'polykey/dist/ids'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as testUtils from '../utils'; + +describe('renew', () => { + const logger = new Logger('renew test', LogLevel.WARN, [new StreamHandler()]); + const password = 'helloworld'; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let oldNodeId: NodeId; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'polykey'); + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + oldNodeId = pkAgent.keyRing.getNodeId(); + }, globalThis.defaultTimeout * 2); + afterEach(async () => { + await pkAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'renews the keypair', + async () => { + // Can't test with target executable due to mocking + // Get previous keypair and nodeId + let { exitCode, stdout } = await testUtils.pkStdio( + ['keys', 'keypair', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + PK_PASSWORD_NEW: 'some-password', + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + const prevPublicKey = JSON.parse(stdout).publicKey; + const prevPrivateKey = JSON.parse(stdout).privateKey; + ({ exitCode, stdout } = await testUtils.pkStdio( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + const prevNodeId = JSON.parse(stdout).nodeId; + // Renew keypair + const passPath = path.join(dataDir, 'renew-password'); + await fs.promises.writeFile(passPath, 'password-new'); + ({ exitCode } = await testUtils.pkStdio( + ['keys', 'renew', '--password-new-file', passPath], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Get new keypair and nodeId and compare against old + ({ exitCode, stdout } = await testUtils.pkStdio( + ['keys', 'keypair', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: 'password-new', + PK_PASSWORD_NEW: 'some-password', + // Client server still using old nodeId, this should be removed if + // this is fixed. + PK_NODE_ID: nodesUtils.encodeNodeId(oldNodeId), + PK_CLIENT_HOST: '127.0.0.1', + PK_CLIENT_PORT: `${pkAgent.webSocketServerClient.getPort()}`, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + const newPublicKey = JSON.parse(stdout).publicKey; + const newPrivateKey = JSON.parse(stdout).privateKey; + ({ exitCode, stdout } = await testUtils.pkStdio( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: 'password-new', + // Client server still using old nodeId, this should be removed if + // this is fixed. + PK_NODE_ID: nodesUtils.encodeNodeId(oldNodeId), + PK_CLIENT_HOST: '127.0.0.1', + PK_CLIENT_PORT: `${pkAgent.webSocketServerClient.getPort()}`, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + const newNodeId = JSON.parse(stdout).nodeId; + expect(newPublicKey).not.toBe(prevPublicKey); + expect(newPrivateKey).not.toBe(prevPrivateKey); + expect(newNodeId).not.toBe(prevNodeId); + }, + ); +}); diff --git a/tests/keys/reset.test.ts b/tests/keys/reset.test.ts new file mode 100644 index 00000000..2241d841 --- /dev/null +++ b/tests/keys/reset.test.ts @@ -0,0 +1,133 @@ +import type { Host } from 'polykey/dist/network/types'; +import type { NodeId } from 'polykey/dist/ids'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as testUtils from '../utils'; + +describe('reset', () => { + const logger = new Logger('reset test', LogLevel.WARN, [new StreamHandler()]); + const password = 'helloworld'; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let oldNodeId: NodeId; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'polykey'); + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + oldNodeId = pkAgent.keyRing.getNodeId(); + }, globalThis.defaultTimeout * 2); + afterEach(async () => { + await pkAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'resets the keypair', + async () => { + // Can't test with target executable due to mocking + // Get previous keypair and nodeId + let { exitCode, stdout } = await testUtils.pkStdio( + ['keys', 'keypair', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + PK_PASSWORD_NEW: 'some-password', + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + const prevPublicKey = JSON.parse(stdout).publicKey; + const prevPrivateKey = JSON.parse(stdout).privateKey; + ({ exitCode, stdout } = await testUtils.pkStdio( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + const prevNodeId = JSON.parse(stdout).nodeId; + // Reset keypair + const passPath = path.join(dataDir, 'reset-password'); + await fs.promises.writeFile(passPath, 'password-new'); + ({ exitCode } = await testUtils.pkStdio( + ['keys', 'reset', '--password-new-file', passPath], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + // Get new keypair and nodeId and compare against old + ({ exitCode, stdout } = await testUtils.pkStdio( + ['keys', 'keypair', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: 'password-new', + PK_PASSWORD_NEW: 'some-password', + // Client server still using old nodeId, this should be removed if + // this is fixed. + PK_NODE_ID: nodesUtils.encodeNodeId(oldNodeId), + PK_CLIENT_HOST: '127.0.0.1', + PK_CLIENT_PORT: `${pkAgent.webSocketServerClient.getPort()}`, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + const newPublicKey = JSON.parse(stdout).publicKey; + const newPrivateKey = JSON.parse(stdout).privateKey; + ({ exitCode, stdout } = await testUtils.pkStdio( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: 'password-new', + // Client server still using old nodeId, this should be removed if + // this is fixed. + PK_NODE_ID: nodesUtils.encodeNodeId(oldNodeId), + PK_CLIENT_HOST: '127.0.0.1', + PK_CLIENT_PORT: `${pkAgent.webSocketServerClient.getPort()}`, + }, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + const newNodeId = JSON.parse(stdout).nodeId; + expect(newPublicKey).not.toBe(prevPublicKey); + expect(newPrivateKey).not.toBe(prevPrivateKey); + expect(newNodeId).not.toBe(prevNodeId); + }, + ); +}); diff --git a/tests/keys/signVerify.test.ts b/tests/keys/signVerify.test.ts new file mode 100644 index 00000000..ce184065 --- /dev/null +++ b/tests/keys/signVerify.test.ts @@ -0,0 +1,163 @@ +import type { StatusLive } from 'polykey/dist/status/types'; +import type { Signature } from 'polykey/dist/keys/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import sysexits from 'polykey/dist/utils/sysexits'; +import * as testUtils from '../utils'; + +describe('sign-verify', () => { + const logger = new Logger('sign-verify test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + let agentStatus: StatusLive; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose, agentStatus } = + await testUtils.setupTestAgent(logger)); + }); + afterEach(async () => { + await agentClose(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('signs a file', async () => { + const publicKey = keysUtils.publicKeyFromNodeId(agentStatus.data.nodeId); + const dataPath = path.join(agentDir, 'data'); + await fs.promises.writeFile(dataPath, 'sign-me', { + encoding: 'binary', + }); + const { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'sign', dataPath, '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + signature: expect.any(String), + }); + const signed = JSON.parse(stdout).signature; + + expect( + keysUtils.verifyWithPublicKey( + publicKey, + Buffer.from('sign-me'), + Buffer.from(signed, 'binary') as Signature, + ), + ).toBeTrue(); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('verifies a signature with NodeId', async () => { + const sourceKeyPair = keysUtils.generateKeyPair(); + const nodeId = keysUtils.publicKeyToNodeId(sourceKeyPair.publicKey); + const dataPath = path.join(agentDir, 'data'); + await fs.promises.writeFile(dataPath, 'sign-me', { + encoding: 'binary', + }); + const signed = keysUtils.signWithPrivateKey( + sourceKeyPair, + Buffer.from('sign-me', 'binary'), + ); + const signaturePath = path.join(agentDir, 'signature'); + await fs.promises.writeFile(signaturePath, signed, { + encoding: 'binary', + }); + const { exitCode, stdout } = await testUtils.pkExec( + [ + 'keys', + 'verify', + dataPath, + signaturePath, + nodesUtils.encodeNodeId(nodeId), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + signatureVerified: true, + }); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('verifies a signature with JWK', async () => { + const sourceKeyPair = keysUtils.generateKeyPair(); + const jwk = keysUtils.publicKeyToJWK(sourceKeyPair.publicKey); + const dataPath = path.join(agentDir, 'data'); + await fs.promises.writeFile(dataPath, 'sign-me', { + encoding: 'binary', + }); + const signed = keysUtils.signWithPrivateKey( + sourceKeyPair, + Buffer.from('sign-me', 'binary'), + ); + const signaturePath = path.join(agentDir, 'signature'); + await fs.promises.writeFile(signaturePath, signed, { + encoding: 'binary', + }); + const jwkPath = path.join(agentDir, 'jwk'); + await fs.promises.writeFile(jwkPath, JSON.stringify(jwk), { + encoding: 'utf-8', + }); + const { exitCode, stdout } = await testUtils.pkExec( + ['keys', 'verify', dataPath, signaturePath, jwkPath, '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + signatureVerified: true, + }); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('verifies a signature fails with invalid JWK', async () => { + const dataPath = path.join(agentDir, 'data'); + await fs.promises.writeFile(dataPath, 'sign-me', { + encoding: 'binary', + }); + const signed = 'abc'; + const signaturePath = path.join(agentDir, 'signature'); + await fs.promises.writeFile(signaturePath, signed, { + encoding: 'binary', + }); + const jwkPath = path.join(agentDir, 'jwk'); + const { exitCode } = await testUtils.pkExec( + ['keys', 'verify', dataPath, signaturePath, jwkPath, '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + ); + expect(exitCode).toBe(sysexits.NOINPUT); + }); +}); diff --git a/tests/nat/DMZ.test.ts b/tests/nat/DMZ.test.ts new file mode 100644 index 00000000..88cc8520 --- /dev/null +++ b/tests/nat/DMZ.test.ts @@ -0,0 +1,310 @@ +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import readline from 'readline'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import Status from 'polykey/dist/status/Status'; +import config from 'polykey/dist/config'; +import * as testNatUtils from './utils'; +import * as testUtils from '../utils'; +import { + isPlatformLinux, + hasIp, + hasIptables, + hasNsenter, + hasUnshare, +} from '../utils/platform'; + +const supportsNatTesting = + isPlatformLinux && hasIp && hasIptables && hasNsenter && hasUnshare; + +test('dummy test to avoid fail', async () => {}); +// FIXME: disabled NAT testing for now, pending changes in agent migration 2 +describe.skip('DMZ', () => { + const logger = new Logger('DMZ test', LogLevel.WARN, [new StreamHandler()]); + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(supportsNatTesting)( + 'can create an agent in a namespace', + async () => { + const password = 'abc123'; + const usrns = await testNatUtils.createUserNamespace(logger); + const netns = await testNatUtils.createNetworkNamespace( + usrns.pid!, + logger, + ); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'polykey'), + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + '0', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + command: `nsenter ${testNatUtils + .nsenter(usrns.pid!, netns.pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + logger.getChild('agentProcess'), + ); + const rlOut = readline.createInterface(agentProcess.stdout!); + const stdout = await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }); + const statusLiveData = JSON.parse(stdout); + expect(statusLiveData).toMatchObject({ + pid: agentProcess.pid, + nodeId: expect.any(String), + clientHost: expect.any(String), + clientPort: expect.any(Number), + agentHost: expect.any(String), + agentPort: expect.any(Number), + }); + agentProcess.kill('SIGTERM'); + let exitCode, signal; + [exitCode, signal] = await testUtils.processExit(agentProcess); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + // Check for graceful exit + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + statusLockPath: path.join( + dataDir, + 'polykey', + config.defaults.statusLockBase, + ), + fs, + logger, + }); + const statusInfo = (await status.readStatus())!; + expect(statusInfo.status).toBe('DEAD'); + netns.kill('SIGTERM'); + [exitCode, signal] = await testUtils.processExit(netns); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + usrns.kill('SIGTERM'); + [exitCode, signal] = await testUtils.processExit(usrns); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + }, + globalThis.defaultTimeout * 4, + ); + testUtils.testIf(supportsNatTesting)( + 'agents in different namespaces can ping each other', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent1Host, + agent1AgentPort, + agent2NodeId, + agent2Host, + agent2AgentPort, + tearDownNAT, + } = await testNatUtils.setupNAT('dmz', 'dmz', logger); + // Namespace1 Namespace2 + // ┌────────────────────────────────────┐ ┌────────────────────────────────────┐ + // │ │ │ │ + // │ ┌────────┐ ┌─────────┐ │ │ ┌─────────┐ ┌────────┐ │ + // │ │ Agent1 ├────────┤ Router1 │ │ │ │ Router2 ├────────┤ Agent2 │ │ + // │ └────────┘ └─────────┘ │ │ └─────────┘ └────────┘ │ + // │ 10.0.0.2:55551 192.168.0.1:55555 │ │ 192.168.0.2:55555 10.0.0.2:55552 │ + // │ │ │ │ + // └────────────────────────────────────┘ └────────────────────────────────────┘ + // Since neither node is behind a NAT can directly add eachother's + // details using pk nodes add + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent2NodeId, + agent2Host, + agent2AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent1NodeId, + agent1Host, + agent1AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); + testUtils.testIf(supportsNatTesting)( + 'agents in different namespaces can ping each other via seed node', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent2NodeId, + tearDownNAT, + } = await testNatUtils.setupNATWithSeedNode('dmz', 'dmz', logger); + // Namespace1 Namespace3 Namespace2 + // ┌────────────────────────────────────┐ ┌──────────────────┐ ┌────────────────────────────────────┐ + // │ │ │ │ │ │ + // │ ┌────────┐ ┌─────────┐ │ │ ┌──────────┐ │ │ ┌─────────┐ ┌────────┐ │ + // │ │ Agent1 ├────────┤ Router1 │ │ │ │ SeedNode │ │ │ │ Router2 ├────────┤ Agent2 │ │ + // │ └────────┘ └─────────┘ │ │ └──────────┘ │ │ └─────────┘ └────────┘ │ + // │ 10.0.0.2:55551 192.168.0.1:55555 │ │ 192.168.0.3:PORT │ │ 192.168.0.2:55555 10.0.0.2:55552 │ + // │ │ │ │ │ │ + // └────────────────────────────────────┘ └──────────────────┘ └────────────────────────────────────┘ + // Should be able to ping straight away using the details from the + // seed node + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); +}); diff --git a/tests/nat/endpointDependentNAT.test.ts b/tests/nat/endpointDependentNAT.test.ts new file mode 100644 index 00000000..29fcf9a3 --- /dev/null +++ b/tests/nat/endpointDependentNAT.test.ts @@ -0,0 +1,359 @@ +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testNatUtils from './utils'; +import * as testUtils from '../utils'; + +const supportsNatTesting = + testUtils.isPlatformLinux && + testUtils.hasIp && + testUtils.hasIptables && + testUtils.hasNsenter && + testUtils.hasUnshare; + +test('dummy test to avoid fail', async () => {}); +// FIXME: disabled NAT testing for now, pending changes in agent migration 2 +describe.skip('endpoint dependent NAT traversal', () => { + const logger = new Logger('EDM NAT test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(supportsNatTesting)( + 'node1 behind EDM NAT connects to node2', + async () => { + const { + userPid, + agent1Pid, + password, + dataDir, + agent1NodePath, + agent2NodeId, + agent2Host, + agent2AgentPort, + tearDownNAT, + } = await testNatUtils.setupNAT('edm', 'dmz', logger); + // Namespace1 + // ┌────────────────────────────────────────────────────┐ + // │ │ Namespace2 + // │ 55551<->PORT1 192.168.0.1:PORT1 │ ┌────────────────────────────────────┐ + // │ ┌────────┐ ┌─────┐ ┌─────────┐ │ │ │ + // │ │ │ │ ├─────────┤ │ │ │ ┌─────────┐ ┌────────┐ │ + // │ │ Agent1 ├────────┤ NAT │ │ Router1 │ │ │ │ Router2 ├────────┤ Agent2 │ │ + // │ │ │ │ │ │ │ │ │ └─────────┘ └────────┘ │ + // │ └────────┘ └─────┘ └─────────┘ │ │ 192.168.0.2:55555 10.0.0.2:55552 │ + // │ 10.0.0.2:55551 │ │ │ + // │ │ └────────────────────────────────────┘ + // └────────────────────────────────────────────────────┘ + // Since node2 is not behind a NAT can directly add its details + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent2NodeId, + agent2Host, + agent2AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + const { exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); + testUtils.testIf(supportsNatTesting)( + 'node1 connects to node2 behind EDM NAT', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent1Host, + agent1AgentPort, + agent2NodeId, + tearDownNAT, + } = await testNatUtils.setupNAT('dmz', 'edm', logger); + // Namespace2 + // ┌────────────────────────────────────────────────────┐ + // Namespace1 │ │ + // ┌────────────────────────────────────┐ │ 192.168.0.2:PORT1 PORT1<->55552 │ + // │ │ │ ┌─────────┐ ┌─────┐ ┌────────┐ │ + // │ ┌────────┐ ┌─────────┐ │ │ │ ├─────────┤ │ │ │ │ + // │ │ Agent1 ├────────┤ Router1 │ │ │ │ Router2 │ │ NAT ├────────┤ Agent2 │ │ + // │ └────────┘ └─────────┘ │ │ │ │ │ │ │ │ │ + // │ 10.0.0.2:55551 192.168.0.1:55555 │ │ └─────────┘ └─────┘ └────────┘ │ + // │ │ │ 10.0.0.2:55552 │ + // └────────────────────────────────────┘ │ │ + // └────────────────────────────────────────────────────┘ + // Agent 2 must ping Agent 1 first, since Agent 2 is behind a NAT + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent1NodeId, + agent1Host, + agent1AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + // Can now ping Agent 2 (it will be expecting a response) + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); + testUtils.testIf(supportsNatTesting)( + 'node1 behind EDM NAT cannot connect to node2 behind EDM NAT', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent2NodeId, + tearDownNAT, + } = await testNatUtils.setupNATWithSeedNode('edm', 'edm', logger); + // Namespace1 Namespace3 Namespace2 + // ┌────────────────────────────────────────────────────┐ ┌──────────────────┐ ┌────────────────────────────────────────────────────┐ + // │ │ │ │ │ │ + // │ 55551<->PORT1 192.168.0.1:PORT1 │ │ ┌──────────┐ │ │ 192.168.0.2:PORT1 PORT1<->55552 │ + // │ ┌────────┐ ┌─────┐ ┌─────────┐ │ │ │ SeedNode │ │ │ ┌─────────┐ ┌─────┐ ┌────────┐ │ + // │ │ │ │ ├─────────┤ │ │ │ └──────────┘ │ │ │ ├─────────┤ │ │ │ │ + // │ │ Agent1 ├────────┤ NAT │ │ Router1 │ │ │ 192.168.0.3:PORT │ │ │ Router2 │ │ NAT ├────────┤ Agent2 │ │ + // │ │ │ │ ├─────────┤ │ │ │ │ │ │ ├─────────┤ │ │ │ │ + // │ └────────┘ └─────┘ └─────────┘ │ └──────────────────┘ │ └─────────┘ └─────┘ └────────┘ │ + // │ 10.0.0.2:55551 55551<->PORT2 192.168.0.1:PORT2 │ │ 192.168.0.2:PORT2 PORT2<->55552 10.0.0.2:55552 │ + // │ │ │ │ + // └────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────┘ + // Contact details are retrieved from the seed node, but cannot be used + // since port mapping changes between targets in EDM mapping + // Node 2 -> Node 1 ping should fail (Node 1 behind NAT) + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(1); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: expect.any(String), + }); + // Node 1 -> Node 2 ping should also fail for the same reason + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(1); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: expect.any(String), + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); + testUtils.testIf(supportsNatTesting)( + 'node1 behind EDM NAT cannot connect to node2 behind EIM NAT', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent2NodeId, + tearDownNAT, + } = await testNatUtils.setupNATWithSeedNode('edm', 'eim', logger); + // Namespace3 + // ┌──────────────────┐ + // │ │ + // Namespace1 │ ┌──────────┐ │ + // ┌────────────────────────────────────────────────────┐ │ │ SeedNode │ │ + // │ │ │ └──────────┘ │ + // │ 55551<->PORT1 192.168.0.1:PORT1 │ │ 192.168.0.3:PORT │ + // │ ┌────────┐ ┌─────┐ ┌─────────┐ │ │ │ + // │ │ │ │ ├─────────┤ │ │ └──────────────────┘ + // │ │ Agent1 ├────────┤ NAT │ │ Router1 │ │ Namespace2 + // │ │ │ │ ├─────────┤ │ │ ┌──────────────────────────────────────────────────┐ + // │ └────────┘ └─────┘ └─────────┘ │ │ │ + // │ 10.0.0.2:55551 55551<->PORT2 192.168.0.1:PORT2 │ │ ┌─────────┐ ┌─────┐ ┌────────┐ │ + // │ │ │ │ Router2 ├───────┤ NAT ├────────┤ Agent2 │ │ + // └────────────────────────────────────────────────────┘ │ └─────────┘ └─────┘ └────────┘ │ + // │ 192.168.0.2:PORT PORT<->55552 10.0.0.2:55552 │ + // │ │ + // └──────────────────────────────────────────────────┘ + // Since one of the nodes uses EDM NAT we cannot punch through + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(1); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: expect.any(String), + }); + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(1); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: expect.any(String), + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); +}); diff --git a/tests/nat/endpointIndependentNAT.test.ts b/tests/nat/endpointIndependentNAT.test.ts new file mode 100644 index 00000000..5641c471 --- /dev/null +++ b/tests/nat/endpointIndependentNAT.test.ts @@ -0,0 +1,536 @@ +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testNatUtils from './utils'; +import * as testUtils from '../utils'; + +const supportsNatTesting = + testUtils.isPlatformLinux && + testUtils.hasIp && + testUtils.hasIptables && + testUtils.hasNsenter && + testUtils.hasUnshare; + +const disabled = false; + +test('dummy test to avoid fail', async () => {}); +// FIXME: disabled NAT testing for now, pending changes in agent migration 2 +describe.skip('endpoint independent NAT traversal', () => { + const logger = new Logger('EIM NAT test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(supportsNatTesting)( + 'node1 behind EIM NAT connects to node2', + async () => { + const { + userPid, + agent1Pid, + password, + dataDir, + agent1NodePath, + agent2NodeId, + agent2Host, + agent2AgentPort, + tearDownNAT, + } = await testNatUtils.setupNAT('eim', 'dmz', logger); + // Namespace1 Namespace2 + // ┌──────────────────────────────────────────────────┐ ┌────────────────────────────────────┐ + // │ │ │ │ + // │ ┌────────┐ ┌─────┐ ┌─────────┐ │ │ ┌─────────┐ ┌────────┐ │ + // │ │ Agent1 ├───────┤ NAT ├─────────┤ Router1 │ │ │ │ Router2 ├────────┤ Agent2 │ │ + // │ └────────┘ └─────┘ └─────────┘ │ │ └─────────┘ └────────┘ │ + // │ 10.0.0.2:55551 55551<->PORT 192.168.0.1:PORT │ │ 192.168.0.2:55555 10.0.0.2:55552 │ + // │ │ │ │ + // └──────────────────────────────────────────────────┘ └────────────────────────────────────┘ + // Since node2 is not behind a NAT can directly add its details + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent2NodeId, + agent2Host, + agent2AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + const { exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); + testUtils.testIf(supportsNatTesting)( + 'node1 connects to node2 behind EIM NAT', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent1Host, + agent1AgentPort, + agent2NodeId, + agent2Host, + agent2AgentPort, + tearDownNAT, + } = await testNatUtils.setupNAT('dmz', 'eim', logger); + // Namespace1 Namespace2 + // ┌────────────────────────────────────┐ ┌──────────────────────────────────────────────────┐ + // │ │ │ │ + // │ ┌────────┐ ┌─────────┐ │ │ ┌─────────┐ ┌─────┐ ┌────────┐ │ + // │ │ Agent1 ├────────┤ Router1 │ │ │ │ Router2 ├───────┤ NAT ├────────┤ Agent2 │ │ + // │ └────────┘ └─────────┘ │ │ └─────────┘ └─────┘ └────────┘ │ + // │ 10.0.0.2:55551 192.168.0.1:55555 │ │ 192.168.0.2:PORT PORT<->55552 10.0.0.2:55552 │ + // │ │ │ │ + // └────────────────────────────────────┘ └──────────────────────────────────────────────────┘ + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent1NodeId, + agent1Host, + agent1AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent2NodeId, + agent2Host, + agent2AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + // If we try to ping Agent 2 it will fail + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(1); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: 'No response received', + }); + // But Agent 2 can ping Agent 1 because Agent 1 is not behind a NAT + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + // Can now ping Agent 2 (it will be expecting a response) + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); + testUtils.testIf(supportsNatTesting)( + 'node1 behind EIM NAT connects to node2 behind EIM NAT', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent1Host, + agent1AgentPort, + agent2NodeId, + agent2Host, + agent2AgentPort, + tearDownNAT, + } = await testNatUtils.setupNAT('eim', 'eim', logger); + // Namespace1 Namespace2 + // ┌──────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────┐ + // │ │ │ │ + // │ ┌────────┐ ┌─────┐ ┌─────────┐ │ │ ┌─────────┐ ┌─────┐ ┌────────┐ │ + // │ │ Agent1 ├───────┤ NAT ├─────────┤ Router1 │ │ │ │ Router2 ├───────┤ NAT ├────────┤ Agent2 │ │ + // │ └────────┘ └─────┘ └─────────┘ │ │ └─────────┘ └─────┘ └────────┘ │ + // │ 10.0.0.2:55551 55551<->PORT 192.168.0.1:PORT │ │ 192.168.0.2:PORT PORT<->55552 10.0.0.2:55552 │ + // │ │ │ │ + // └──────────────────────────────────────────────────┘ └──────────────────────────────────────────────────┘ + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent1NodeId, + agent1Host, + agent1AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + await testUtils.pkExec( + [ + 'nodes', + 'add', + agent2NodeId, + agent2Host, + agent2AgentPort, + '--no-ping', + ], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + ); + // If we try to ping Agent 2 it will fail + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(1); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: 'No response received', + }); + // But Agent 2 can ping Agent 1 because it's expecting a response now + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + // Can now ping Agent 2 (it will be expecting a response too) + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); + // FIXME: known issue, disabled for now + testUtils.testIf(disabled && supportsNatTesting)( + 'node1 behind EIM NAT connects to node2 behind EIM NAT via seed node', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent2NodeId, + tearDownNAT, + } = await testNatUtils.setupNATWithSeedNode('eim', 'eim', logger); + // Namespace1 Namespace3 Namespace2 + // ┌──────────────────────────────────────────────────┐ ┌──────────────────┐ ┌──────────────────────────────────────────────────┐ + // │ │ │ │ │ │ + // │ ┌────────┐ ┌─────┐ ┌─────────┐ │ │ ┌──────────┐ │ │ ┌─────────┐ ┌─────┐ ┌────────┐ │ + // │ │ Agent1 ├───────┤ NAT ├─────────┤ Router1 │ │ │ │ SeedNode │ │ │ │ Router2 ├───────┤ NAT ├────────┤ Agent2 │ │ + // │ └────────┘ └─────┘ └─────────┘ │ │ └──────────┘ │ │ └─────────┘ └─────┘ └────────┘ │ + // │ 10.0.0.2:55551 55551<->PORT 192.168.0.1:PORT │ │ 192.168.0.3:PORT │ │ 192.168.0.2:PORT PORT<->55552 10.0.0.2:55552 │ + // │ │ │ │ │ │ + // └──────────────────────────────────────────────────┘ └──────────────────┘ └──────────────────────────────────────────────────┘ + // Should be able to ping straight away using the seed node as a + // signaller + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); + testUtils.testIf(supportsNatTesting)( + 'node1 behind EIM NAT cannot connect to node2 behind EDM NAT', + async () => { + const { + userPid, + agent1Pid, + agent2Pid, + password, + dataDir, + agent1NodePath, + agent2NodePath, + agent1NodeId, + agent2NodeId, + tearDownNAT, + } = await testNatUtils.setupNATWithSeedNode('eim', 'edm', logger); + // Namespace3 + // ┌──────────────────┐ + // │ │ + // │ ┌──────────┐ │ Namespace2 + // │ │ SeedNode │ │ ┌────────────────────────────────────────────────────┐ + // │ └──────────┘ │ │ │ + // │ 192.168.0.3:PORT │ │ 192.168.0.2:PORT1 PORT1<->55552 │ + // │ │ │ ┌─────────┐ ┌─────┐ ┌────────┐ │ + // └──────────────────┘ │ │ ├─────────┤ │ │ │ │ + // Namespace1 │ │ Router2 │ │ NAT ├────────┤ Agent2 │ │ + // ┌──────────────────────────────────────────────────┐ │ │ ├─────────┤ │ │ │ │ + // │ │ │ └─────────┘ └─────┘ └────────┘ │ + // │ ┌────────┐ ┌─────┐ ┌─────────┐ │ │ 192.168.0.2:PORT2 PORT2<->55552 10.0.0.2:55552 │ + // │ │ Agent1 ├───────┤ NAT ├─────────┤ Router1 │ │ │ │ + // │ └────────┘ └─────┘ └─────────┘ │ └────────────────────────────────────────────────────┘ + // │ 10.0.0.2:55551 55551<->PORT 192.168.0.1:PORT │ + // │ │ + // └──────────────────────────────────────────────────┘ + // Since one of the nodes uses EDM NAT we cannot punch through + let exitCode, stdout; + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent1NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent2NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent2Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(1); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: expect.any(String), + }); + ({ exitCode, stdout } = await testUtils.pkExec( + ['nodes', 'ping', agent2NodeId, '--format', 'json'], + { + env: { + PK_NODE_PATH: agent1NodePath, + PK_PASSWORD: password, + }, + command: `nsenter ${testNatUtils + .nsenter(userPid!, agent1Pid!) + .join(' ')} ts-node --project ${testUtils.tsConfigPath} ${ + testUtils.polykeyPath + }`, + cwd: dataDir, + }, + )); + expect(exitCode).toBe(1); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: expect.any(String), + }); + await tearDownNAT(); + }, + globalThis.defaultTimeout * 4, + ); +}); diff --git a/tests/nat/utils.ts b/tests/nat/utils.ts new file mode 100644 index 00000000..6bebbf32 --- /dev/null +++ b/tests/nat/utils.ts @@ -0,0 +1,1405 @@ +import type { ChildProcess } from 'child_process'; +import os from 'os'; +import fs from 'fs'; +import path from 'path'; +import readline from 'readline'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from '../utils'; + +type NATType = 'eim' | 'edm' | 'dmz'; + +/** + * Veth end for Agent 1 + * Connects to Router 1 + */ +const AGENT1_VETH = 'agent1'; +/** + * Veth end for Agent 2 + * Connects to Router 2 + */ +const AGENT2_VETH = 'agent2'; +/** + * Internal veth end for Router 1 + * Connects to Agent 1 + */ +const ROUTER1_VETH_INT = 'router1-int'; +/** + * External veth end for Router 1 + * Connects to Router 2 + */ +const ROUTER1_VETH_EXT = 'router1-ext'; +/** + * Internal veth end for Router 2 + * Connects to Agent 2 + */ +const ROUTER2_VETH_INT = 'router2-int'; +/** + * External veth end for Router 2 + * Connects to Router 1 + */ +const ROUTER2_VETH_EXT = 'router2-ext'; +/** + * External veth end for Router 1 + * Connects to a seed node + */ +const ROUTER1_VETH_SEED = 'router1-seed'; +/** + * External veth end for Router 2 + * Connects to a seed node + */ +const ROUTER2_VETH_SEED = 'router2-seed'; +/** + * Veth end for a seed node + * Connects to Router 1 + */ +const SEED_VETH_ROUTER1 = 'seed-router1'; +/** + * Veth end for a seed node + * Connects to Router 2 + */ +const SEED_VETH_ROUTER2 = 'seed-router2'; + +/** + * Subnet for Agent 1 + */ +const AGENT1_HOST = '10.0.0.2'; +/** + * Subnet for Agent 2 + */ +const AGENT2_HOST = '10.0.0.2'; +/** + * Subnet for internal communication from Router 1 + * Forwards to Agent 1 + */ +const ROUTER1_HOST_INT = '10.0.0.1'; +/** + * Subnet for internal communication from Router 2 + * Forwards to Agent 2 + */ +const ROUTER2_HOST_INT = '10.0.0.1'; +/** + * Subnet for external communication from Router 1 + * Forwards to Router 2 + */ +const ROUTER1_HOST_EXT = '192.168.0.1'; +/** + * Subnet for external communication from Router 2 + * Forwards to Router 1 + */ +const ROUTER2_HOST_EXT = '192.168.0.2'; +/** + * Subnet for external communication from Router 1 + * Forwards to a seed node + */ +const ROUTER1_HOST_SEED = '192.168.0.1'; +/** + * Subnet for external communication from a seed node + */ +const SEED_HOST = '192.168.0.3'; +/** + * Subnet for external communication from Router 2 + * Forwards to a seed node + */ +const ROUTER2_HOST_SEED = '192.168.0.2'; + +/** + * Subnet mask + */ +const SUBNET_MASK = '/24'; + +/** + * Port on Agent 1 + */ +const AGENT1_PORT = '55551'; +/** + * Port on Agent 2 + */ +const AGENT2_PORT = '55552'; +/** + * Mapped port for DMZ + */ +const DMZ_PORT = '55555'; + +/** + * Formats the command to enter a namespace to run a process inside it + */ +const nsenter = (usrnsPid: number, netnsPid: number) => { + return [ + '--target', + usrnsPid.toString(), + '--user', + '--preserve-credentials', + 'nsenter', + '--target', + netnsPid.toString(), + '--net', + ]; +}; + +/** + * Create a user namespace from which network namespaces can be created without + * requiring sudo + */ +async function createUserNamespace( + logger: Logger = new Logger(createUserNamespace.name), +): Promise { + logger.info('unshare --user --map-root-user'); + const subprocess = await testUtils.spawn( + 'unshare', + ['--user', '--map-root-user'], + { env: {} }, + logger, + ); + return subprocess; +} + +/** + * Create a network namespace inside a user namespace + */ +async function createNetworkNamespace( + usrnsPid: number, + logger: Logger = new Logger(createNetworkNamespace.name), +): Promise { + logger.info( + `nsenter --target ${usrnsPid.toString()} --user --preserve-credentials unshare --net`, + ); + const subprocess = await testUtils.spawn( + 'nsenter', + [ + '--target', + usrnsPid.toString(), + '--user', + '--preserve-credentials', + 'unshare', + '--net', + ], + { env: {} }, + logger, + ); + return subprocess; +} + +/** + * Set up four network namespaces to allow communication between two agents + * each behind a router + * Brings up loopback interfaces, creates and brings up a veth pair + * between each pair of adjacent namespaces, and adds default routing to allow + * cross-communication + */ +async function setupNetworkNamespaceInterfaces( + usrnsPid: number, + agent1NetnsPid: number, + router1NetnsPid: number, + router2NetnsPid: number, + agent2NetnsPid: number, + logger: Logger = new Logger(setupNetworkNamespaceInterfaces.name), +) { + let args: Array = []; + try { + // Bring up loopback + args = [ + ...nsenter(usrnsPid, agent1NetnsPid), + 'ip', + 'link', + 'set', + 'lo', + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'link', + 'set', + 'lo', + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'link', + 'set', + 'lo', + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, agent2NetnsPid), + 'ip', + 'link', + 'set', + 'lo', + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Create veth pair to link the namespaces + args = [ + ...nsenter(usrnsPid, agent1NetnsPid), + 'ip', + 'link', + 'add', + AGENT1_VETH, + 'type', + 'veth', + 'peer', + 'name', + ROUTER1_VETH_INT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'link', + 'add', + ROUTER1_VETH_EXT, + 'type', + 'veth', + 'peer', + 'name', + ROUTER2_VETH_EXT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'link', + 'add', + ROUTER2_VETH_INT, + 'type', + 'veth', + 'peer', + 'name', + AGENT2_VETH, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Link up the ends to the correct namespaces + args = [ + ...nsenter(usrnsPid, agent1NetnsPid), + 'ip', + 'link', + 'set', + 'dev', + ROUTER1_VETH_INT, + 'netns', + router1NetnsPid.toString(), + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'link', + 'set', + 'dev', + ROUTER2_VETH_EXT, + 'netns', + router2NetnsPid.toString(), + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'link', + 'set', + 'dev', + AGENT2_VETH, + 'netns', + agent2NetnsPid.toString(), + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Bring up each end + args = [ + ...nsenter(usrnsPid, agent1NetnsPid), + 'ip', + 'link', + 'set', + AGENT1_VETH, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'link', + 'set', + ROUTER1_VETH_INT, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'link', + 'set', + ROUTER1_VETH_EXT, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'link', + 'set', + ROUTER2_VETH_EXT, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'link', + 'set', + ROUTER2_VETH_INT, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, agent2NetnsPid), + 'ip', + 'link', + 'set', + AGENT2_VETH, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Assign ip addresses to each end + args = [ + ...nsenter(usrnsPid, agent1NetnsPid), + 'ip', + 'addr', + 'add', + `${AGENT1_HOST}${SUBNET_MASK}`, + 'dev', + AGENT1_VETH, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'addr', + 'add', + `${ROUTER1_HOST_INT}${SUBNET_MASK}`, + 'dev', + ROUTER1_VETH_INT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'addr', + 'add', + `${ROUTER1_HOST_EXT}${SUBNET_MASK}`, + 'dev', + ROUTER1_VETH_EXT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'addr', + 'add', + `${ROUTER2_HOST_EXT}${SUBNET_MASK}`, + 'dev', + ROUTER2_VETH_EXT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'addr', + 'add', + `${ROUTER2_HOST_INT}${SUBNET_MASK}`, + 'dev', + ROUTER2_VETH_INT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, agent2NetnsPid), + 'ip', + 'addr', + 'add', + `${AGENT2_HOST}${SUBNET_MASK}`, + 'dev', + AGENT2_VETH, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Add default routing + args = [ + ...nsenter(usrnsPid, agent1NetnsPid), + 'ip', + 'route', + 'add', + 'default', + 'via', + ROUTER1_HOST_INT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'route', + 'add', + 'default', + 'via', + ROUTER2_HOST_EXT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'route', + 'add', + 'default', + 'via', + ROUTER1_HOST_EXT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, agent2NetnsPid), + 'ip', + 'route', + 'add', + 'default', + 'via', + ROUTER2_HOST_INT, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + } catch (e) { + logger.error(e.message); + } +} + +/** + * Set up four network namespaces to allow communication between two agents + * each behind a router + * Brings up loopback interfaces, creates and brings up a veth pair + * between each pair of adjacent namespaces, and adds default routing to allow + * cross-communication + */ +async function setupSeedNamespaceInterfaces( + usrnsPid: number, + seedNetnsPid: number, + router1NetnsPid: number, + router2NetnsPid: number, + logger: Logger = new Logger(setupSeedNamespaceInterfaces.name), +) { + let args: Array = []; + try { + // Bring up loopback + args = [ + ...nsenter(usrnsPid, seedNetnsPid), + 'ip', + 'link', + 'set', + 'lo', + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Create veth pairs to link the namespaces + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'link', + 'add', + ROUTER1_VETH_SEED, + 'type', + 'veth', + 'peer', + 'name', + SEED_VETH_ROUTER1, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'link', + 'add', + ROUTER2_VETH_SEED, + 'type', + 'veth', + 'peer', + 'name', + SEED_VETH_ROUTER2, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Move seed ends into seed network namespace + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'link', + 'set', + 'dev', + SEED_VETH_ROUTER1, + 'netns', + seedNetnsPid.toString(), + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'link', + 'set', + 'dev', + SEED_VETH_ROUTER2, + 'netns', + seedNetnsPid.toString(), + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Bring up each end + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'link', + 'set', + ROUTER1_VETH_SEED, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, seedNetnsPid), + 'ip', + 'link', + 'set', + SEED_VETH_ROUTER1, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, seedNetnsPid), + 'ip', + 'link', + 'set', + SEED_VETH_ROUTER2, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'link', + 'set', + ROUTER2_VETH_SEED, + 'up', + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Assign ip addresses to each end + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'addr', + 'add', + `${ROUTER1_HOST_SEED}${SUBNET_MASK}`, + 'dev', + ROUTER1_VETH_SEED, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, seedNetnsPid), + 'ip', + 'addr', + 'add', + `${SEED_HOST}${SUBNET_MASK}`, + 'dev', + SEED_VETH_ROUTER1, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, seedNetnsPid), + 'ip', + 'addr', + 'add', + `${SEED_HOST}${SUBNET_MASK}`, + 'dev', + SEED_VETH_ROUTER2, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'addr', + 'add', + `${ROUTER2_HOST_SEED}${SUBNET_MASK}`, + 'dev', + ROUTER2_VETH_SEED, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + // Add default routing + args = [ + ...nsenter(usrnsPid, router1NetnsPid), + 'ip', + 'route', + 'add', + SEED_HOST, + 'dev', + ROUTER1_VETH_SEED, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, router2NetnsPid), + 'ip', + 'route', + 'add', + SEED_HOST, + 'dev', + ROUTER2_VETH_SEED, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, seedNetnsPid), + 'ip', + 'route', + 'add', + ROUTER1_HOST_SEED, + 'dev', + SEED_VETH_ROUTER1, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + args = [ + ...nsenter(usrnsPid, seedNetnsPid), + 'ip', + 'route', + 'add', + ROUTER2_HOST_SEED, + 'dev', + SEED_VETH_ROUTER2, + ]; + logger.info(['nsenter', ...args].join(' ')); + await testUtils.exec('nsenter', args); + } catch (e) { + logger.error(e.message); + } +} + +/** + * Setup routing between an agent and router with no NAT rules + */ +async function setupDMZ( + usrnsPid: number, + routerNsPid: number, + agentIp: string, + agentPort: string, + routerExt: string, + routerExtIp: string, + logger: Logger = new Logger(setupDMZ.name), +) { + const postroutingCommand = [ + ...nsenter(usrnsPid, routerNsPid), + 'iptables', + '--table', + 'nat', + '--append', + 'POSTROUTING', + '--protocol', + 'udp', + '--source', + `${agentIp}${SUBNET_MASK}`, + '--out-interface', + routerExt, + '--jump', + 'SNAT', + '--to-source', + `${routerExtIp}:${DMZ_PORT}`, + ]; + const preroutingCommand = [ + ...nsenter(usrnsPid, routerNsPid), + 'iptables', + '--table', + 'nat', + '--append', + 'PREROUTING', + '--protocol', + 'udp', + '--destination-port', + DMZ_PORT, + '--in-interface', + routerExt, + '--jump', + 'DNAT', + '--to-destination', + `${agentIp}:${agentPort}`, + ]; + try { + logger.info(['nsenter', ...postroutingCommand].join(' ')); + await testUtils.exec('nsenter', postroutingCommand); + logger.info(['nsenter', ...preroutingCommand].join(' ')); + await testUtils.exec('nsenter', preroutingCommand); + } catch (e) { + logger.error(e.message); + } +} + +/** + * Setup Port-Restricted Cone NAT for a namespace (on the router namespace) + */ +async function setupNATEndpointIndependentMapping( + usrnsPid: number, + routerNsPid: number, + agentIp: string, + routerExt: string, + routerInt: string, + logger: Logger = new Logger(setupNATEndpointIndependentMapping.name), +) { + const natCommand = [ + ...nsenter(usrnsPid, routerNsPid), + 'iptables', + '--table', + 'nat', + '--append', + 'POSTROUTING', + '--protocol', + 'udp', + '--source', + `${agentIp}${SUBNET_MASK}`, + '--out-interface', + routerExt, + '--jump', + 'MASQUERADE', + ]; + const acceptLocalCommand = [ + ...nsenter(usrnsPid, routerNsPid), + 'iptables', + '--table', + 'filter', + '--append', + 'INPUT', + '--in-interface', + routerInt, + '--jump', + 'ACCEPT', + ]; + const acceptEstablishedCommand = [ + ...nsenter(usrnsPid, routerNsPid), + 'iptables', + '--table', + 'filter', + '--append', + 'INPUT', + '--match', + 'conntrack', + '--ctstate', + 'RELATED,ESTABLISHED', + '--jump', + 'ACCEPT', + ]; + const dropCommand = [ + ...nsenter(usrnsPid, routerNsPid), + 'iptables', + '--table', + 'filter', + '--append', + 'INPUT', + '--jump', + 'DROP', + ]; + try { + logger.info(['nsenter', ...acceptLocalCommand].join(' ')); + await testUtils.exec('nsenter', acceptLocalCommand); + logger.info(['nsenter', ...acceptEstablishedCommand].join(' ')); + await testUtils.exec('nsenter', acceptEstablishedCommand); + logger.info(['nsenter', ...dropCommand].join(' ')); + await testUtils.exec('nsenter', dropCommand); + logger.info(['nsenter', ...natCommand].join(' ')); + await testUtils.exec('nsenter', natCommand); + } catch (e) { + logger.error(e.message); + } +} + +/** + * Setup Symmetric NAT for a namespace (on the router namespace) + */ +async function setupNATEndpointDependentMapping( + usrnsPid: number, + routerNsPid: number, + routerExt: string, + logger: Logger = new Logger(setupNATEndpointDependentMapping.name), +) { + const command = [ + ...nsenter(usrnsPid, routerNsPid), + 'iptables', + '--table', + 'nat', + '--append', + 'POSTROUTING', + '--protocol', + 'udp', + '--out-interface', + routerExt, + '--jump', + 'MASQUERADE', + `--random`, + ]; + try { + logger.info(['nsenter', ...command].join(' ')); + await testUtils.exec('nsenter', command); + } catch (e) { + logger.error(e.message); + } +} + +async function setupNATWithSeedNode( + agent1NAT: NATType, + agent2NAT: NATType, + logger: Logger = new Logger(setupNAT.name, LogLevel.WARN, [ + new StreamHandler(), + ]), +) { + const dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const password = 'password'; + // Create a user namespace containing five network namespaces + // Two agents, two routers, one seed node + const usrns = await createUserNamespace(logger); + const seedNetns = await createNetworkNamespace(usrns.pid!, logger); + const agent1Netns = await createNetworkNamespace(usrns.pid!, logger); + const agent2Netns = await createNetworkNamespace(usrns.pid!, logger); + const router1Netns = await createNetworkNamespace(usrns.pid!, logger); + const router2Netns = await createNetworkNamespace(usrns.pid!, logger); + // Apply appropriate NAT rules + switch (agent1NAT) { + case 'dmz': { + await setupDMZ( + usrns.pid!, + router1Netns.pid!, + AGENT1_HOST, + AGENT1_PORT, + ROUTER1_VETH_EXT, + ROUTER1_HOST_EXT, + logger, + ); + await setupDMZ( + usrns.pid!, + router1Netns.pid!, + AGENT1_HOST, + AGENT1_PORT, + ROUTER1_VETH_SEED, + ROUTER1_HOST_SEED, + logger, + ); + break; + } + case 'eim': { + await setupNATEndpointIndependentMapping( + usrns.pid!, + router1Netns.pid!, + AGENT1_HOST, + ROUTER1_VETH_EXT, + ROUTER1_VETH_INT, + logger, + ); + await setupNATEndpointIndependentMapping( + usrns.pid!, + router1Netns.pid!, + AGENT1_HOST, + ROUTER1_VETH_SEED, + ROUTER1_VETH_INT, + logger, + ); + break; + } + case 'edm': { + await setupNATEndpointDependentMapping( + usrns.pid!, + router1Netns.pid!, + ROUTER1_VETH_EXT, + logger, + ); + await setupNATEndpointDependentMapping( + usrns.pid!, + router1Netns.pid!, + ROUTER1_VETH_SEED, + logger, + ); + break; + } + } + switch (agent2NAT) { + case 'dmz': { + await setupDMZ( + usrns.pid!, + router2Netns.pid!, + AGENT2_HOST, + AGENT2_PORT, + ROUTER2_VETH_EXT, + ROUTER2_HOST_EXT, + logger, + ); + await setupDMZ( + usrns.pid!, + router2Netns.pid!, + AGENT2_HOST, + AGENT2_PORT, + ROUTER2_VETH_SEED, + ROUTER2_HOST_SEED, + logger, + ); + break; + } + case 'eim': { + await setupNATEndpointIndependentMapping( + usrns.pid!, + router2Netns.pid!, + AGENT2_HOST, + ROUTER2_VETH_EXT, + ROUTER2_VETH_INT, + logger, + ); + await setupNATEndpointIndependentMapping( + usrns.pid!, + router2Netns.pid!, + AGENT2_HOST, + ROUTER2_VETH_SEED, + ROUTER2_VETH_INT, + logger, + ); + break; + } + case 'edm': { + await setupNATEndpointDependentMapping( + usrns.pid!, + router2Netns.pid!, + ROUTER2_VETH_EXT, + logger, + ); + await setupNATEndpointDependentMapping( + usrns.pid!, + router2Netns.pid!, + ROUTER2_VETH_SEED, + logger, + ); + break; + } + } + await setupNetworkNamespaceInterfaces( + usrns.pid!, + agent1Netns.pid!, + router1Netns.pid!, + router2Netns.pid!, + agent2Netns.pid!, + logger, + ); + await setupSeedNamespaceInterfaces( + usrns.pid!, + seedNetns.pid!, + router1Netns.pid!, + router2Netns.pid!, + logger, + ); + const seedNode = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'seed'), + '--client-host', + '127.0.0.1', + '--agent-host', + '0.0.0.0', + '--connection-timeout', + '1000', + '--workers', + '0', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + command: `nsenter ${nsenter(usrns.pid!, seedNetns.pid!).join( + ' ', + )} ts-node --project ${testUtils.tsConfigPath} ${testUtils.polykeyPath}`, + cwd: dataDir, + }, + logger.getChild('seed'), + ); + const rlOutSeed = readline.createInterface(seedNode.stdout!); + const stdoutSeed = await new Promise((resolve, reject) => { + rlOutSeed.once('line', resolve); + rlOutSeed.once('close', reject); + }); + const nodeIdSeed = JSON.parse(stdoutSeed).nodeId; + const agentPortSeed = JSON.parse(stdoutSeed).agentPort; + const agent1 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'agent1'), + '--client-host', + '127.0.0.1', + '--agent-host', + `${AGENT1_HOST}`, + '--agent-port', + `${AGENT1_PORT}`, + '--workers', + '0', + '--connection-timeout', + '1000', + '--seed-nodes', + `${nodeIdSeed}@${SEED_HOST}:${agentPortSeed}`, + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + command: `nsenter ${nsenter(usrns.pid!, agent1Netns.pid!).join( + ' ', + )} ts-node --project ${testUtils.tsConfigPath} ${testUtils.polykeyPath}`, + cwd: dataDir, + }, + logger.getChild('agent1'), + ); + const rlOutNode1 = readline.createInterface(agent1.stdout!); + const stdoutNode1 = await new Promise((resolve, reject) => { + rlOutNode1.once('line', resolve); + rlOutNode1.once('close', reject); + }); + const nodeId1 = JSON.parse(stdoutNode1).nodeId; + const agent2 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'agent2'), + '--client-host', + '127.0.0.1', + '--agent-host', + `${AGENT2_HOST}`, + '--agent-port', + `${AGENT2_PORT}`, + '--workers', + '0', + '--connection-timeout', + '1000', + '--seed-nodes', + `${nodeIdSeed}@${SEED_HOST}:${agentPortSeed}`, + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + command: `nsenter ${nsenter(usrns.pid!, agent2Netns.pid!).join( + ' ', + )} ts-node --project ${testUtils.tsConfigPath} ${testUtils.polykeyPath}`, + cwd: dataDir, + }, + logger.getChild('agent2'), + ); + const rlOutNode2 = readline.createInterface(agent2.stdout!); + const stdoutNode2 = await new Promise((resolve, reject) => { + rlOutNode2.once('line', resolve); + rlOutNode2.once('close', reject); + }); + const nodeId2 = JSON.parse(stdoutNode2).nodeId; + return { + userPid: usrns.pid, + agent1Pid: agent1Netns.pid, + agent2Pid: agent2Netns.pid, + password, + dataDir, + agent1NodePath: path.join(dataDir, 'agent1'), + agent2NodePath: path.join(dataDir, 'agent2'), + agent1NodeId: nodeId1, + agent2NodeId: nodeId2, + tearDownNAT: async () => { + agent2.kill('SIGTERM'); + await testUtils.processExit(agent2); + agent1.kill('SIGTERM'); + await testUtils.processExit(agent1); + seedNode.kill('SIGTERM'); + await testUtils.processExit(seedNode); + router2Netns.kill('SIGTERM'); + await testUtils.processExit(router2Netns); + router1Netns.kill('SIGTERM'); + await testUtils.processExit(router1Netns); + agent2Netns.kill('SIGTERM'); + await testUtils.processExit(agent2Netns); + agent1Netns.kill('SIGTERM'); + await testUtils.processExit(agent1Netns); + seedNetns.kill('SIGTERM'); + await testUtils.processExit(seedNetns); + usrns.kill('SIGTERM'); + await testUtils.processExit(usrns); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }, + }; +} + +async function setupNAT( + agent1NAT: NATType, + agent2NAT: NATType, + logger: Logger = new Logger(setupNAT.name, LogLevel.WARN, [ + new StreamHandler(), + ]), +) { + const dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const password = 'password'; + // Create a user namespace containing four network namespaces + // Two agents and two routers + const usrns = await createUserNamespace(logger); + const agent1Netns = await createNetworkNamespace(usrns.pid!, logger); + const agent2Netns = await createNetworkNamespace(usrns.pid!, logger); + const router1Netns = await createNetworkNamespace(usrns.pid!, logger); + const router2Netns = await createNetworkNamespace(usrns.pid!, logger); + // Apply appropriate NAT rules + switch (agent1NAT) { + case 'dmz': { + await setupDMZ( + usrns.pid!, + router1Netns.pid!, + AGENT1_HOST, + AGENT1_PORT, + ROUTER1_VETH_EXT, + ROUTER1_HOST_EXT, + logger, + ); + break; + } + case 'eim': { + await setupNATEndpointIndependentMapping( + usrns.pid!, + router1Netns.pid!, + AGENT1_HOST, + ROUTER1_VETH_EXT, + ROUTER1_VETH_INT, + logger, + ); + break; + } + case 'edm': { + await setupNATEndpointDependentMapping( + usrns.pid!, + router1Netns.pid!, + ROUTER1_VETH_EXT, + logger, + ); + break; + } + } + switch (agent2NAT) { + case 'dmz': { + await setupDMZ( + usrns.pid!, + router2Netns.pid!, + AGENT2_HOST, + AGENT2_PORT, + ROUTER2_VETH_EXT, + ROUTER2_HOST_EXT, + logger, + ); + break; + } + case 'eim': { + await setupNATEndpointIndependentMapping( + usrns.pid!, + router2Netns.pid!, + AGENT2_HOST, + ROUTER2_VETH_EXT, + ROUTER2_VETH_INT, + logger, + ); + break; + } + case 'edm': { + await setupNATEndpointDependentMapping( + usrns.pid!, + router2Netns.pid!, + ROUTER2_VETH_EXT, + logger, + ); + break; + } + } + await setupNetworkNamespaceInterfaces( + usrns.pid!, + agent1Netns.pid!, + router1Netns.pid!, + router2Netns.pid!, + agent2Netns.pid!, + logger, + ); + const agent1 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'agent1'), + '--client-host', + '127.0.0.1', + '--agent-host', + `${AGENT1_HOST}`, + '--agent-port', + `${AGENT1_PORT}`, + '--connection-timeout', + '1000', + '--workers', + '0', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + command: `nsenter ${nsenter(usrns.pid!, agent1Netns.pid!).join( + ' ', + )} ts-node --project ${testUtils.tsConfigPath} ${testUtils.polykeyPath}`, + cwd: dataDir, + }, + logger.getChild('agent1'), + ); + const rlOutNode1 = readline.createInterface(agent1.stdout!); + const stdoutNode1 = await new Promise((resolve, reject) => { + rlOutNode1.once('line', resolve); + rlOutNode1.once('close', reject); + }); + const nodeId1 = JSON.parse(stdoutNode1).nodeId; + const agent2 = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'agent2'), + '--client-host', + '127.0.0.1', + '--agent-host', + `${AGENT2_HOST}`, + '--agent-port', + `${AGENT2_PORT}`, + '--connection-timeout', + '1000', + '--workers', + '0', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + command: `nsenter ${nsenter(usrns.pid!, agent2Netns.pid!).join( + ' ', + )} ts-node --project ${testUtils.tsConfigPath} ${testUtils.polykeyPath}`, + cwd: dataDir, + }, + logger.getChild('agent2'), + ); + const rlOutNode2 = readline.createInterface(agent2.stdout!); + const stdoutNode2 = await new Promise((resolve, reject) => { + rlOutNode2.once('line', resolve); + rlOutNode2.once('close', reject); + }); + const nodeId2 = JSON.parse(stdoutNode2).nodeId; + return { + userPid: usrns.pid, + agent1Pid: agent1Netns.pid, + agent2Pid: agent2Netns.pid, + password, + dataDir, + agent1NodePath: path.join(dataDir, 'agent1'), + agent2NodePath: path.join(dataDir, 'agent2'), + agent1NodeId: nodeId1, + agent1Host: ROUTER1_HOST_EXT, + agent1AgentPort: agent1NAT === 'dmz' ? DMZ_PORT : AGENT1_PORT, + agent2NodeId: nodeId2, + agent2Host: ROUTER2_HOST_EXT, + agent2AgentPort: agent2NAT === 'dmz' ? DMZ_PORT : AGENT2_PORT, + tearDownNAT: async () => { + agent2.kill('SIGTERM'); + await testUtils.processExit(agent2); + agent1.kill('SIGTERM'); + await testUtils.processExit(agent1); + router2Netns.kill('SIGTERM'); + await testUtils.processExit(router2Netns); + router1Netns.kill('SIGTERM'); + await testUtils.processExit(router1Netns); + agent2Netns.kill('SIGTERM'); + await testUtils.processExit(agent2Netns); + agent1Netns.kill('SIGTERM'); + await testUtils.processExit(agent1Netns); + usrns.kill('SIGTERM'); + await testUtils.processExit(usrns); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }, + }; +} + +export { + nsenter, + setupNAT, + setupNATWithSeedNode, + createUserNamespace, + createNetworkNamespace, + setupNetworkNamespaceInterfaces, +}; diff --git a/tests/nodes/add.test.ts b/tests/nodes/add.test.ts new file mode 100644 index 00000000..e6b57c72 --- /dev/null +++ b/tests/nodes/add.test.ts @@ -0,0 +1,211 @@ +import type { NodeId } from 'polykey/dist/ids/types'; +import type { Host } from 'polykey/dist/network/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { IdInternal } from '@matrixai/id'; +import { sysexits } from 'polykey/dist/utils'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import NodeManager from 'polykey/dist/nodes/NodeManager'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from '../utils'; + +describe('add', () => { + const logger = new Logger('add test', LogLevel.WARN, [new StreamHandler()]); + const password = 'helloworld'; + const validNodeId = testUtils.generateRandomNodeId(); + const invalidNodeId = IdInternal.fromString('INVALIDID'); + const validHost = '0.0.0.0'; + const invalidHost = 'INVALIDHOST'; + const port = 55555; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let mockedPingNode: jest.SpyInstance; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'polykey'); + mockedPingNode = jest.spyOn(NodeManager.prototype, 'pingNode'); + // Cannot use the shared global agent since we can't 'un-add' a node + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + await pkAgent.nodeGraph.stop(); + await pkAgent.nodeGraph.start({ fresh: true }); + mockedPingNode.mockImplementation(() => true); + }); + afterEach(async () => { + await pkAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + mockedPingNode.mockRestore(); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)('adds a node', async () => { + const { exitCode } = await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(validNodeId), + validHost, + `${port}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + // Checking if node was added. + const { stdout } = await testUtils.pkStdio( + ['nodes', 'find', nodesUtils.encodeNodeId(validNodeId)], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(stdout).toContain(validHost); + expect(stdout).toContain(`${port}`); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'fails to add a node (invalid node ID)', + async () => { + const { exitCode } = await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(invalidNodeId), + validHost, + `${port}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(sysexits.USAGE); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'fails to add a node (invalid IP address)', + async () => { + const { exitCode } = await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(validNodeId), + invalidHost, + `${port}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(sysexits.USAGE); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'adds a node with --force flag', + async () => { + const { exitCode } = await testUtils.pkStdio( + [ + 'nodes', + 'add', + '--force', + nodesUtils.encodeNodeId(validNodeId), + validHost, + `${port}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + // Checking if node was added. + const node = await pkAgent.nodeGraph.getNode(validNodeId); + expect(node?.address).toEqual({ host: validHost, port: port }); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'fails to add node when ping fails', + async () => { + mockedPingNode.mockImplementation(() => false); + const { exitCode } = await testUtils.pkStdio( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(validNodeId), + validHost, + `${port}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(sysexits.NOHOST); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'adds a node with --no-ping flag', + async () => { + mockedPingNode.mockImplementation(() => false); + const { exitCode } = await testUtils.pkStdio( + [ + 'nodes', + 'add', + '--no-ping', + nodesUtils.encodeNodeId(validNodeId), + validHost, + `${port}`, + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + // Checking if node was added. + const node = await pkAgent.nodeGraph.getNode(validNodeId); + expect(node?.address).toEqual({ host: validHost, port: port }); + }, + ); +}); diff --git a/tests/nodes/claim.test.ts b/tests/nodes/claim.test.ts new file mode 100644 index 00000000..e9094968 --- /dev/null +++ b/tests/nodes/claim.test.ts @@ -0,0 +1,139 @@ +import type { NodeId, NodeIdEncoded } from 'polykey/dist/ids/types'; +import type { Host } from 'polykey/dist/network/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from '../utils'; + +describe('claim', () => { + const logger = new Logger('claim test', LogLevel.WARN, [new StreamHandler()]); + const password = 'helloworld'; + let dataDir: string; + let nodePath: string; + let pkAgent: PolykeyAgent; + let remoteNode: PolykeyAgent; + let localId: NodeId; + let remoteId: NodeId; + let remoteIdEncoded: NodeIdEncoded; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'keynode'); + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + seedNodes: {}, // Explicitly no seed nodes on startup + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + localId = pkAgent.keyRing.getNodeId(); + // Setting up a remote keynode + remoteNode = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: path.join(dataDir, 'remoteNode'), + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + seedNodes: {}, // Explicitly no seed nodes on startup + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + remoteId = remoteNode.keyRing.getNodeId(); + remoteIdEncoded = nodesUtils.encodeNodeId(remoteId); + await testUtils.nodesConnect(pkAgent, remoteNode); + await pkAgent.acl.setNodePerm(remoteId, { + gestalt: { + notify: null, + claim: null, + }, + vaults: {}, + }); + await remoteNode.acl.setNodePerm(localId, { + gestalt: { + notify: null, + claim: null, + }, + vaults: {}, + }); + }); + afterEach(async () => { + await pkAgent.stop(); + await remoteNode.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'sends a gestalt invite', + async () => { + const { exitCode, stdout } = await testUtils.pkStdio( + ['nodes', 'claim', remoteIdEncoded], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(stdout).toContain('Successfully generated a cryptolink claim'); + expect(stdout).toContain(remoteIdEncoded); + }, + ); + // TestUtils.testIf(testUtils.isTestPlatformEmpty) + test('sends a gestalt invite (force invite)', async () => { + await remoteNode.notificationsManager.sendNotification(localId, { + type: 'GestaltInvite', + }); + const { exitCode, stdout } = await testUtils.pkStdio( + ['nodes', 'claim', remoteIdEncoded, '--force-invite'], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(stdout).toContain('Successfully generated a cryptolink'); + expect(stdout).toContain(nodesUtils.encodeNodeId(remoteId)); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)('claims a node', async () => { + await remoteNode.notificationsManager.sendNotification(localId, { + type: 'GestaltInvite', + }); + const { exitCode, stdout } = await testUtils.pkStdio( + ['nodes', 'claim', remoteIdEncoded], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(stdout).toContain('cryptolink claim'); + expect(stdout).toContain(remoteIdEncoded); + }); +}); diff --git a/tests/nodes/find.test.ts b/tests/nodes/find.test.ts new file mode 100644 index 00000000..eeb0cb84 --- /dev/null +++ b/tests/nodes/find.test.ts @@ -0,0 +1,192 @@ +import type { Host, Port } from 'polykey/dist/network/types'; +import type { NodeId } from 'polykey/dist/ids/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import { sysexits } from 'polykey/dist/errors'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from '../utils'; + +describe('find', () => { + const logger = new Logger('find test', LogLevel.WARN, [new StreamHandler()]); + const password = 'helloworld'; + let dataDir: string; + let nodePath: string; + let polykeyAgent: PolykeyAgent; + let remoteOnline: PolykeyAgent; + let remoteOffline: PolykeyAgent; + let remoteOnlineNodeId: NodeId; + let remoteOfflineNodeId: NodeId; + let remoteOnlineHost: Host; + let remoteOnlinePort: Port; + let remoteOfflineHost: Host; + let remoteOfflinePort: Port; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'keynode'); + polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + nodeConnectionManagerConfig: { + connectionConnectTime: 2000, + connectionTimeoutTime: 2000, + }, + seedNodes: {}, // Explicitly no seed nodes on startup + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + // Setting up a remote keynode + remoteOnline = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: path.join(dataDir, 'remoteOnline'), + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + remoteOnlineNodeId = remoteOnline.keyRing.getNodeId(); + remoteOnlineHost = remoteOnline.quicServerAgent.host as unknown as Host; + remoteOnlinePort = remoteOnline.quicServerAgent.port as unknown as Port; + await testUtils.nodesConnect(polykeyAgent, remoteOnline); + // Setting up an offline remote keynode + remoteOffline = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: path.join(dataDir, 'remoteOffline'), + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + remoteOfflineNodeId = remoteOffline.keyRing.getNodeId(); + remoteOfflineHost = remoteOffline.quicServerAgent.host as unknown as Host; + remoteOfflinePort = remoteOffline.quicServerAgent.port as unknown as Port; + await testUtils.nodesConnect(polykeyAgent, remoteOffline); + await remoteOffline.stop(); + }); + afterEach(async () => { + await polykeyAgent.stop(); + await remoteOnline.stop(); + await remoteOffline.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'finds an online node', + async () => { + const { exitCode, stdout } = await testUtils.pkStdio( + [ + 'nodes', + 'find', + nodesUtils.encodeNodeId(remoteOnlineNodeId), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: `Found node at ${remoteOnlineHost}:${remoteOnlinePort}`, + id: nodesUtils.encodeNodeId(remoteOnlineNodeId), + host: remoteOnlineHost, + port: remoteOnlinePort, + }); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'finds an offline node', + async () => { + const { exitCode, stdout } = await testUtils.pkStdio( + [ + 'nodes', + 'find', + nodesUtils.encodeNodeId(remoteOfflineNodeId), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: `Found node at ${remoteOfflineHost}:${remoteOfflinePort}`, + id: nodesUtils.encodeNodeId(remoteOfflineNodeId), + host: remoteOfflineHost, + port: remoteOfflinePort, + }); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'fails to find an unknown node', + async () => { + const unknownNodeId = nodesUtils.decodeNodeId( + 'vrcacp9vsb4ht25hds6s4lpp2abfaso0mptcfnh499n35vfcn2gkg', + ); + const { exitCode, stdout } = await testUtils.pkStdio( + [ + 'nodes', + 'find', + nodesUtils.encodeNodeId(unknownNodeId!), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(sysexits.GENERAL); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: `Failed to find node ${nodesUtils.encodeNodeId( + unknownNodeId!, + )}`, + id: nodesUtils.encodeNodeId(unknownNodeId!), + host: '', + port: 0, + }); + }, + globalThis.failedConnectionTimeout, + ); +}); diff --git a/tests/nodes/ping.test.ts b/tests/nodes/ping.test.ts new file mode 100644 index 00000000..6a1bae51 --- /dev/null +++ b/tests/nodes/ping.test.ts @@ -0,0 +1,175 @@ +import type { NodeId } from 'polykey/dist/ids/types'; +import type { Host } from 'polykey/dist/network/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import { sysexits } from 'polykey/dist/errors'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from '../utils'; + +describe('ping', () => { + const logger = new Logger('ping test', LogLevel.WARN, [new StreamHandler()]); + const password = 'helloworld'; + let dataDir: string; + let nodePath: string; + let polykeyAgent: PolykeyAgent; + let remoteOnline: PolykeyAgent; + let remoteOffline: PolykeyAgent; + let remoteOnlineNodeId: NodeId; + let remoteOfflineNodeId: NodeId; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'keynode'); + polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + nodeConnectionManagerConfig: { + connectionConnectTime: 2000, + connectionTimeoutTime: 1000, + }, + seedNodes: {}, // Explicitly no seed nodes on startup + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + // Setting up a remote keynode + remoteOnline = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: path.join(dataDir, 'remoteOnline'), + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + remoteOnlineNodeId = remoteOnline.keyRing.getNodeId(); + await testUtils.nodesConnect(polykeyAgent, remoteOnline); + // Setting up an offline remote keynode + remoteOffline = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: path.join(dataDir, 'remoteOffline'), + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + remoteOfflineNodeId = remoteOffline.keyRing.getNodeId(); + await testUtils.nodesConnect(polykeyAgent, remoteOffline); + await remoteOffline.stop(); + }); + afterEach(async () => { + await polykeyAgent.stop(); + await remoteOnline.stop(); + await remoteOffline.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'fails when pinging an offline node', + async () => { + const { exitCode, stdout, stderr } = await testUtils.pkStdio( + [ + 'nodes', + 'ping', + nodesUtils.encodeNodeId(remoteOfflineNodeId), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(sysexits.GENERAL); // Should fail with no response. for automation purposes. + expect(stderr).toContain('No response received'); + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: 'No response received', + }); + }, + globalThis.failedConnectionTimeout, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'fails if node cannot be found', + async () => { + const fakeNodeId = nodesUtils.decodeNodeId( + 'vrsc24a1er424epq77dtoveo93meij0pc8ig4uvs9jbeld78n9nl0', + ); + const { exitCode, stdout } = await testUtils.pkStdio( + [ + 'nodes', + 'ping', + nodesUtils.encodeNodeId(fakeNodeId!), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).not.toBe(0); // Should fail if node doesn't exist. + expect(JSON.parse(stdout)).toEqual({ + success: false, + message: `No response received`, + }); + }, + globalThis.failedConnectionTimeout, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'succeed when pinging a live node', + async () => { + const { exitCode, stdout } = await testUtils.pkStdio( + [ + 'nodes', + 'ping', + nodesUtils.encodeNodeId(remoteOnlineNodeId), + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: nodePath, + PK_PASSWORD: password, + }, + cwd: dataDir, + }, + ); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout)).toEqual({ + success: true, + message: 'Node is Active.', + }); + }, + ); +}); diff --git a/tests/notifications/sendReadClear.test.ts b/tests/notifications/sendReadClear.test.ts new file mode 100644 index 00000000..a0f8fb39 --- /dev/null +++ b/tests/notifications/sendReadClear.test.ts @@ -0,0 +1,337 @@ +import type { NodeId } from 'polykey/dist/ids/types'; +import type { Notification } from 'polykey/dist/notifications/types'; +import type { StatusLive } from 'polykey/dist/status/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as testUtils from '../utils'; + +describe('send/read/claim', () => { + const logger = new Logger('send/read/clear test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let dataDir: string; + let senderId: NodeId; + let senderHost: string; + let senderPort: number; + let receiverId: NodeId; + let receiverHost: string; + let receiverPort: number; + let senderAgentStatus: StatusLive; + let senderAgentClose: () => Promise; + let senderAgentDir: string; + let senderAgentPassword: string; + let receiverAgentStatus: StatusLive; + let receiverAgentClose: () => Promise; + let receiverAgentDir: string; + let receiverAgentPassword: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + // Cannot use the shared global agent since we can't 'un-add' a node + // which we need in order to trust it and send notifications to it + ({ + agentStatus: senderAgentStatus, + agentClose: senderAgentClose, + agentDir: senderAgentDir, + agentPassword: senderAgentPassword, + } = await testUtils.setupTestAgent(logger)); + senderId = senderAgentStatus.data.nodeId; + senderHost = senderAgentStatus.data.agentHost; + senderPort = senderAgentStatus.data.agentPort; + ({ + agentStatus: receiverAgentStatus, + agentClose: receiverAgentClose, + agentDir: receiverAgentDir, + agentPassword: receiverAgentPassword, + } = await testUtils.setupTestAgent(logger)); + receiverId = receiverAgentStatus.data.nodeId; + receiverHost = receiverAgentStatus.data.agentHost; + receiverPort = receiverAgentStatus.data.agentPort; + }); + afterEach(async () => { + await receiverAgentClose(); + await senderAgentClose(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )( + 'sends, receives, and clears notifications', + async () => { + let exitCode, stdout; + let readNotifications: Array; + // Add receiver to sender's node graph so it can be contacted + ({ exitCode } = await testUtils.pkExec( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(receiverId), + receiverHost, + receiverPort.toString(), + ], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + // Add sender to receiver's node graph so it can be trusted + ({ exitCode } = await testUtils.pkExec( + [ + 'nodes', + 'add', + nodesUtils.encodeNodeId(senderId), + senderHost, + senderPort.toString(), + ], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + // Trust sender so notification can be received + ({ exitCode } = await testUtils.pkExec( + ['identities', 'trust', nodesUtils.encodeNodeId(senderId)], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + // Send some notifications + ({ exitCode } = await testUtils.pkExec( + [ + 'notifications', + 'send', + nodesUtils.encodeNodeId(receiverId), + 'test message 1', + ], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + ({ exitCode } = await testUtils.pkExec( + [ + 'notifications', + 'send', + nodesUtils.encodeNodeId(receiverId), + 'test message 2', + ], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + ({ exitCode } = await testUtils.pkExec( + [ + 'notifications', + 'send', + nodesUtils.encodeNodeId(receiverId), + 'test message 3', + ], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + // Read notifications + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'read', '--format', 'json'], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + readNotifications = stdout + .split('\n') + .slice(undefined, -1) + .map(JSON.parse); + expect(readNotifications).toHaveLength(3); + expect(readNotifications[0]).toMatchObject({ + data: { + type: 'General', + message: 'test message 3', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: nodesUtils.encodeNodeId(receiverId), + isRead: true, + }); + expect(readNotifications[1]).toMatchObject({ + data: { + type: 'General', + message: 'test message 2', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: nodesUtils.encodeNodeId(receiverId), + isRead: true, + }); + expect(readNotifications[2]).toMatchObject({ + data: { + type: 'General', + message: 'test message 1', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: nodesUtils.encodeNodeId(receiverId), + isRead: true, + }); + // Read only unread (none) + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'read', '--unread', '--format', 'json'], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + readNotifications = stdout + .split('\n') + .slice(undefined, -1) + .map(JSON.parse); + expect(readNotifications).toHaveLength(0); + // Read notifications on reverse order + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'read', '--order=oldest', '--format', 'json'], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + readNotifications = stdout + .split('\n') + .slice(undefined, -1) + .map(JSON.parse); + expect(readNotifications).toHaveLength(3); + expect(readNotifications[0]).toMatchObject({ + data: { + type: 'General', + message: 'test message 1', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: nodesUtils.encodeNodeId(receiverId), + isRead: true, + }); + expect(readNotifications[1]).toMatchObject({ + data: { + type: 'General', + message: 'test message 2', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: nodesUtils.encodeNodeId(receiverId), + isRead: true, + }); + expect(readNotifications[2]).toMatchObject({ + data: { + type: 'General', + message: 'test message 3', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: nodesUtils.encodeNodeId(receiverId), + isRead: true, + }); + // Read only one notification + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'read', '--number=1', '--format', 'json'], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + readNotifications = stdout + .split('\n') + .slice(undefined, -1) + .map(JSON.parse); + expect(readNotifications).toHaveLength(1); + expect(readNotifications[0]).toMatchObject({ + data: { + type: 'General', + message: 'test message 3', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: nodesUtils.encodeNodeId(receiverId), + isRead: true, + }); + // Clear notifications + ({ exitCode } = await testUtils.pkExec(['notifications', 'clear'], { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + command: globalThis.testCmd, + })); + // Check there are no more notifications + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'read', '--format', 'json'], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + command: globalThis.testCmd, + }, + )); + expect(exitCode).toBe(0); + readNotifications = stdout + .split('\n') + .slice(undefined, -1) + .map(JSON.parse); + expect(readNotifications).toHaveLength(0); + }, + globalThis.defaultTimeout * 3, + ); +}); diff --git a/tests/polykey.test.ts b/tests/polykey.test.ts new file mode 100644 index 00000000..1e22f8ce --- /dev/null +++ b/tests/polykey.test.ts @@ -0,0 +1,76 @@ +import fs from 'fs'; +import path from 'path'; +import readline from 'readline'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from './utils'; + +describe('polykey', () => { + testUtils.testIf( + testUtils.isTestPlatformEmpty || + testUtils.isTestPlatformLinux || + testUtils.isTestPlatformDocker, + )('default help display', async () => { + const result = await testUtils.pkExec([]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr.length > 0).toBe(true); + }); + testUtils.testIf( + testUtils.isTestPlatformEmpty || testUtils.isTestPlatformDocker, + )('format option affects STDERR', async () => { + const logger = new Logger('format test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + const password = 'abc123'; + const polykeyPath = path.join(dataDir, 'polykey'); + await fs.promises.mkdir(polykeyPath); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'polykey'), + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--verbose', + '--format', + 'json', + ], + { + env: { + PK_TEST_DATA_PATH: dataDir, + PK_PASSWORD: password, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: dataDir, + command: globalThis.testCmd, + }, + logger, + ); + const rlErr = readline.createInterface(agentProcess.stderr!); + // Just check the first log + const stderrStart = await new Promise((resolve, reject) => { + rlErr.once('line', resolve); + rlErr.once('close', reject); + }); + const stderrParsed = JSON.parse(stderrStart); + expect(stderrParsed).toMatchObject({ + level: expect.stringMatching(/INFO|WARN|ERROR|DEBUG/), + keys: expect.any(String), + msg: expect.any(String), + }); + agentProcess.kill('SIGTERM'); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); +}); diff --git a/tests/scratch.test.ts b/tests/scratch.test.ts new file mode 100644 index 00000000..642dd471 --- /dev/null +++ b/tests/scratch.test.ts @@ -0,0 +1,42 @@ +import fs from 'fs'; +import path from 'path'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; + +// This is a 'scratch paper' test file for quickly running tests in the CI +describe('scratch', () => { + const logger = new Logger(`scratch test`, LogLevel.WARN, [ + new StreamHandler(), + ]); + + let dataDir: string; + let nodePath: string; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + nodePath = path.join(dataDir, 'node'); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('can create an agent', async () => { + const pk = await PolykeyAgent.createPolykeyAgent({ + password: 'password', + nodePath, + fresh: true, + logger, + }); + await pk.stop(); + }); +}); + +// We can't have empty test files so here is a sanity test +test('Should avoid empty test suite', async () => { + expect(1 + 1).toBe(2); +}); diff --git a/tests/secrets/secrets.test.ts b/tests/secrets/secrets.test.ts new file mode 100644 index 00000000..a3cb2c8d --- /dev/null +++ b/tests/secrets/secrets.test.ts @@ -0,0 +1,354 @@ +import type { VaultName } from 'polykey/dist/vaults/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { vaultOps } from 'polykey/dist/vaults'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from '../utils'; + +describe('CLI secrets', () => { + const password = 'password'; + const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]); + let dataDir: string; + let polykeyAgent: PolykeyAgent; + let passwordFile: string; + let command: Array; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + passwordFile = path.join(dataDir, 'passwordFile'); + await fs.promises.writeFile(passwordFile, 'password'); + polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: dataDir, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + logger: logger, + }); + // Authorize session + await testUtils.pkStdio( + ['agent', 'unlock', '-np', dataDir, '--password-file', passwordFile], + { + env: {}, + cwd: dataDir, + }, + ); + }); + afterEach(async () => { + await polykeyAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + describe('commandCreateSecret', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should create secrets', + async () => { + const vaultName = 'Vault1' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const secretPath = path.join(dataDir, 'secret'); + await fs.promises.writeFile(secretPath, 'this is a secret'); + + command = [ + 'secrets', + 'create', + '-np', + dataDir, + secretPath, + `${vaultName}:MySecret`, + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(['MySecret']); + expect( + (await vaultOps.getSecret(vault, 'MySecret')).toString(), + ).toStrictEqual('this is a secret'); + }); + }, + globalThis.defaultTimeout * 2, + ); + }); + describe('commandDeleteSecret', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should delete secrets', + async () => { + const vaultName = 'Vault2' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret(vault, 'MySecret', 'this is the secret'); + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(['MySecret']); + }); + + command = [ + 'secrets', + 'delete', + '-np', + dataDir, + `${vaultName}:MySecret`, + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([]); + }); + }, + ); + }); + describe('commandGetSecret', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should retrieve secrets', + async () => { + const vaultName = 'Vault3' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret(vault, 'MySecret', 'this is the secret'); + }); + + command = ['secrets', 'get', '-np', dataDir, `${vaultName}:MySecret`]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.stdout).toBe('this is the secret'); + expect(result.exitCode).toBe(0); + }, + ); + }); + describe('commandListSecrets', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should list secrets', + async () => { + const vaultName = 'Vault4' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret(vault, 'MySecret1', 'this is the secret 1'); + await vaultOps.addSecret(vault, 'MySecret2', 'this is the secret 2'); + await vaultOps.addSecret(vault, 'MySecret3', 'this is the secret 3'); + }); + + command = ['secrets', 'list', '-np', dataDir, vaultName]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + }, + globalThis.defaultTimeout * 2, + ); + }); + describe('commandNewDir', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should make a directory', + async () => { + const vaultName = 'Vault5' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + command = [ + 'secrets', + 'mkdir', + '-np', + dataDir, + `${vaultName}:dir1/dir2`, + '-r', + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret( + vault, + 'dir1/MySecret1', + 'this is the secret 1', + ); + await vaultOps.addSecret( + vault, + 'dir1/dir2/MySecret2', + 'this is the secret 2', + ); + + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual( + ['dir1/MySecret1', 'dir1/dir2/MySecret2'].sort(), + ); + }); + }, + ); + }); + describe('commandRenameSecret', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should rename secrets', + async () => { + const vaultName = 'Vault6' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret(vault, 'MySecret', 'this is the secret'); + }); + + command = [ + 'secrets', + 'rename', + '-np', + dataDir, + `${vaultName}:MySecret`, + 'MyRenamedSecret', + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(['MyRenamedSecret']); + }); + }, + ); + }); + describe('commandUpdateSecret', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should update secrets', + async () => { + const vaultName = 'Vault7' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + const secretPath = path.join(dataDir, 'secret'); + await fs.promises.writeFile(secretPath, 'updated-content'); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret(vault, 'MySecret', 'original-content'); + expect( + (await vaultOps.getSecret(vault, 'MySecret')).toString(), + ).toStrictEqual('original-content'); + }); + + command = [ + 'secrets', + 'update', + '-np', + dataDir, + secretPath, + `${vaultName}:MySecret`, + ]; + + const result2 = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result2.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(['MySecret']); + expect( + (await vaultOps.getSecret(vault, 'MySecret')).toString(), + ).toStrictEqual('updated-content'); + }); + }, + ); + }); + describe('commandNewDirSecret', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should add a directory of secrets', + async () => { + const vaultName = 'Vault8' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + const secretDir = path.join(dataDir, 'secrets'); + await fs.promises.mkdir(secretDir); + await fs.promises.writeFile( + path.join(secretDir, 'secret-1'), + 'this is the secret 1', + ); + await fs.promises.writeFile( + path.join(secretDir, 'secret-2'), + 'this is the secret 2', + ); + await fs.promises.writeFile( + path.join(secretDir, 'secret-3'), + 'this is the secret 3', + ); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([]); + }); + + command = ['secrets', 'dir', '-np', dataDir, secretDir, vaultName]; + + const result2 = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result2.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([ + 'secrets/secret-1', + 'secrets/secret-2', + 'secrets/secret-3', + ]); + }); + }, + ); + }); + describe('commandStat', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should retrieve secrets', + async () => { + const vaultName = 'Vault9'; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret(vault, 'MySecret', 'this is the secret'); + }); + + command = ['secrets', 'stat', '-np', dataDir, `${vaultName}:MySecret`]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('nlink: 1'); + expect(result.stdout).toContain('blocks: 1'); + expect(result.stdout).toContain('blksize: 4096'); + expect(result.stdout).toContain('size: 18'); + }, + ); + }); +}); diff --git a/tests/sessions.test.ts b/tests/sessions.test.ts new file mode 100644 index 00000000..d912c7c6 --- /dev/null +++ b/tests/sessions.test.ts @@ -0,0 +1,176 @@ +/** + * There is no command call sessions + * This is just for testing the CLI Authentication Retry Loop + * @module + */ +import path from 'path'; +import fs from 'fs'; +import prompts from 'prompts'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { Session } from 'polykey/dist/sessions'; +import { sleep } from 'polykey/dist/utils'; +import config from 'polykey/dist/config'; +import * as clientErrors from 'polykey/dist/client/errors'; +import * as testUtils from './utils'; + +jest.mock('prompts'); + +describe('sessions', () => { + const logger = new Logger('sessions test', LogLevel.WARN, [ + new StreamHandler(), + ]); + let agentDir; + let agentPassword; + let agentClose; + let dataDir: string; + beforeEach(async () => { + ({ agentDir, agentPassword, agentClose } = await testUtils.setupTestAgent( + logger, + )); + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + }); + afterEach(async () => { + await sleep(1000); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + await agentClose(); + }); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'serial commands refresh the session token', + async () => { + const session = await Session.createSession({ + sessionTokenPath: path.join(agentDir, config.defaults.tokenBase), + fs, + logger, + }); + let exitCode; + ({ exitCode } = await testUtils.pkStdio(['agent', 'status'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + })); + expect(exitCode).toBe(0); + const token1 = await session.readToken(); + // Tokens are not nonces + // Wait at least 1 second + // To ensure that the next token has a new expiry + await sleep(1100); + ({ exitCode } = await testUtils.pkStdio(['agent', 'status'], { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: agentPassword, + }, + cwd: agentDir, + })); + expect(exitCode).toBe(0); + const token2 = await session.readToken(); + expect(token1).not.toBe(token2); + await session.stop(); + }, + ); + testUtils + .testIf(testUtils.isTestPlatformEmpty) + .only( + 'unattended commands with invalid authentication should fail', + async () => { + let exitCode, stderr; + // Password and Token set + ({ exitCode, stderr } = await testUtils.pkStdio( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: 'invalid', + PK_TOKEN: 'token', + }, + cwd: agentDir, + }, + )); + + testUtils.expectProcessError(exitCode, stderr, [ + new clientErrors.ErrorClientAuthDenied(), + ]); + // Password set + ({ exitCode, stderr } = await testUtils.pkStdio( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: 'invalid', + PK_TOKEN: undefined, + }, + cwd: agentDir, + }, + )); + testUtils.expectProcessError(exitCode, stderr, [ + new clientErrors.ErrorClientAuthDenied(), + ]); + // Token set + ({ exitCode, stderr } = await testUtils.pkStdio( + ['agent', 'status', '--format', 'json'], + { + env: { + PK_NODE_PATH: agentDir, + PK_PASSWORD: undefined, + PK_TOKEN: 'token', + }, + cwd: agentDir, + }, + )); + testUtils.expectProcessError(exitCode, stderr, [ + new clientErrors.ErrorClientAuthDenied(), + ]); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'prompt for password to authenticate attended commands', + async () => { + const password = agentPassword; + await testUtils.pkStdio(['agent', 'lock'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + }); + prompts.mockClear(); + prompts.mockImplementation(async (_opts: any) => { + return { password }; + }); + const { exitCode } = await testUtils.pkStdio(['agent', 'status'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + }); + expect(exitCode).toBe(0); + // Prompted for password 1 time + expect(prompts.mock.calls.length).toBe(1); + prompts.mockClear(); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 're-prompts for password if unable to authenticate command', + async () => { + await testUtils.pkStdio(['agent', 'lock'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + }); + const validPassword = agentPassword; + const invalidPassword = 'invalid'; + prompts.mockClear(); + prompts + .mockResolvedValueOnce({ password: invalidPassword }) + .mockResolvedValue({ password: validPassword }); + const { exitCode } = await testUtils.pkStdio(['agent', 'status'], { + env: { PK_NODE_PATH: agentDir }, + cwd: agentDir, + }); + expect(exitCode).toBe(0); + // Prompted for password 2 times + expect(prompts.mock.calls.length).toBe(2); + prompts.mockClear(); + }, + ); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/setupAfterEnv.ts b/tests/setupAfterEnv.ts new file mode 100644 index 00000000..8ea8279e --- /dev/null +++ b/tests/setupAfterEnv.ts @@ -0,0 +1,4 @@ +// Default timeout per test +// some tests may take longer in which case you should specify the timeout +// explicitly for each test by using the third parameter of test function +jest.setTimeout(globalThis.defaultTimeout); diff --git a/tests/utils.retryAuthentication.test.ts b/tests/utils.retryAuthentication.test.ts new file mode 100644 index 00000000..b30591c5 --- /dev/null +++ b/tests/utils.retryAuthentication.test.ts @@ -0,0 +1,186 @@ +import prompts from 'prompts'; +import mockedEnv from 'mocked-env'; +import * as clientUtils from 'polykey/dist/client/utils'; +import * as clientErrors from 'polykey/dist/client/errors'; +import * as binUtils from '@/utils'; +import * as testUtils from './utils'; + +jest.mock('prompts'); + +describe('bin/utils retryAuthentication', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'no retry on success', + async () => { + const mockCallSuccess = jest.fn().mockResolvedValue('hello world'); + const result = await binUtils.retryAuthentication(mockCallSuccess); + expect(mockCallSuccess.mock.calls.length).toBe(1); + expect(result).toBe('hello world'); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'no retry on generic error', + async () => { + const error = new Error('oh no'); + const mockCallFail = jest.fn().mockRejectedValue(error); + await expect(binUtils.retryAuthentication(mockCallFail)).rejects.toThrow( + /oh no/, + ); + expect(mockCallFail.mock.calls.length).toBe(1); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'no retry on unattended call with PK_TOKEN and PK_PASSWORD', + async () => { + const mockCallFail = jest + .fn() + .mockRejectedValue(new clientErrors.ErrorClientAuthMissing()); + const envRestore = mockedEnv({ + PK_TOKEN: 'hello', + PK_PASSWORD: 'world', + }); + await expect(binUtils.retryAuthentication(mockCallFail)).rejects.toThrow( + clientErrors.ErrorClientAuthMissing, + ); + envRestore(); + expect(mockCallFail.mock.calls.length).toBe(1); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'no retry on unattended call with PK_TOKEN', + async () => { + const mockCallFail = jest + .fn() + .mockRejectedValue(new clientErrors.ErrorClientAuthMissing()); + const envRestore = mockedEnv({ + PK_TOKEN: 'hello', + PK_PASSWORD: undefined, + }); + await expect(binUtils.retryAuthentication(mockCallFail)).rejects.toThrow( + clientErrors.ErrorClientAuthMissing, + ); + envRestore(); + expect(mockCallFail.mock.calls.length).toBe(1); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'no retry on unattended call with PK_PASSWORD', + async () => { + const mockCallFail = jest + .fn() + .mockRejectedValue(new clientErrors.ErrorClientAuthMissing()); + const envRestore = mockedEnv({ + PK_TOKEN: undefined, + PK_PASSWORD: 'world', + }); + await expect(binUtils.retryAuthentication(mockCallFail)).rejects.toThrow( + clientErrors.ErrorClientAuthMissing, + ); + envRestore(); + expect(mockCallFail.mock.calls.length).toBe(1); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'retry once on clientErrors.ErrorClientAuthMissing', + async () => { + const password = 'the password'; + // Password prompt will return hello world + prompts.mockImplementation(async (_opts: any) => { + return { password }; + }); + // Call will reject with ErrorClientAuthMissing then succeed + const mockCall = jest + .fn() + .mockRejectedValueOnce(new clientErrors.ErrorClientAuthMissing()) + .mockResolvedValue('hello world'); + // Make this an attended call + const envRestore = mockedEnv({ + PK_TOKEN: undefined, + PK_PASSWORD: undefined, + }); + const result = await binUtils.retryAuthentication(mockCall); + envRestore(); + // Result is successful + expect(result).toBe('hello world'); + // Call was tried 2 times + expect(mockCall.mock.calls.length).toBe(2); + // Prompted for password 1 time + expect(prompts.mock.calls.length).toBe(1); + // Authorization metadata was set + const auth = mockCall.mock.calls[1][0].authorization; + expect(auth).toBeDefined(); + expect(auth).toBe(clientUtils.encodeAuthFromPassword(password)); + prompts.mockClear(); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'retry 2 times on clientErrors.ErrorClientAuthDenied', + async () => { + const password1 = 'first password'; + const password2 = 'second password'; + prompts.mockClear(); + prompts + .mockResolvedValueOnce({ password: password1 }) + .mockResolvedValue({ password: password2 }); + // Call will reject with ErrorClientAuthMissing then succeed + const mockCall = jest + .fn() + .mockRejectedValueOnce(new clientErrors.ErrorClientAuthMissing()) + .mockRejectedValueOnce(new clientErrors.ErrorClientAuthDenied()) + .mockResolvedValue('hello world'); + // Make this an attended call + const envRestore = mockedEnv({ + PK_TOKEN: undefined, + PK_PASSWORD: undefined, + }); + const result = await binUtils.retryAuthentication(mockCall); + envRestore(); + // Result is successful + expect(result).toBe('hello world'); + // Call was tried 3 times + expect(mockCall.mock.calls.length).toBe(3); + // Prompted for password 2 times + expect(prompts.mock.calls.length).toBe(2); + // Authorization metadata was set + const auth = mockCall.mock.calls[2][0].authorization; + expect(auth).toBeDefined(); + // Second password succeeded + expect(auth).toBe(clientUtils.encodeAuthFromPassword(password2)); + prompts.mockClear(); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'retry 2+ times on clientErrors.ErrorClientAuthDenied until generic error', + async () => { + const password1 = 'first password'; + const password2 = 'second password'; + prompts.mockClear(); + prompts + .mockResolvedValueOnce({ password: password1 }) + .mockResolvedValue({ password: password2 }); + // Call will reject with ErrorClientAuthMissing then succeed + const mockCall = jest + .fn() + .mockRejectedValueOnce(new clientErrors.ErrorClientAuthMissing()) + .mockRejectedValueOnce(new clientErrors.ErrorClientAuthDenied()) + .mockRejectedValueOnce(new clientErrors.ErrorClientAuthDenied()) + .mockRejectedValueOnce(new clientErrors.ErrorClientAuthDenied()) + .mockRejectedValue(new Error('oh no')); + // Make this an attended call + const envRestore = mockedEnv({ + PK_TOKEN: undefined, + PK_PASSWORD: undefined, + }); + await expect(binUtils.retryAuthentication(mockCall)).rejects.toThrow( + /oh no/, + ); + envRestore(); + expect(mockCall.mock.calls.length).toBe(5); + expect(prompts.mock.calls.length).toBe(4); + const auth = mockCall.mock.calls[4][0].authorization; + expect(auth).toBeDefined(); + // Second password was the last used + expect(auth).toBe(clientUtils.encodeAuthFromPassword(password2)); + prompts.mockClear(); + }, + ); +}); diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 00000000..45d33dd8 --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,195 @@ +import type { Host, Port } from 'polykey/dist/network/types'; +import ErrorPolykey from 'polykey/dist/ErrorPolykey'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as rpcErrors from 'polykey/dist/rpc/errors'; +import * as binUtils from '@/utils/utils'; +import * as testUtils from './utils'; + +describe('bin/utils', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'list in human and json format', + () => { + // List + expect( + binUtils.outputFormatter({ + type: 'list', + data: ['Testing', 'the', 'list', 'output'], + }), + ).toBe('Testing\nthe\nlist\noutput\n'); + // JSON + expect( + binUtils.outputFormatter({ + type: 'json', + data: ['Testing', 'the', 'list', 'output'], + }), + ).toBe('["Testing","the","list","output"]\n'); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'table in human and in json format', + () => { + // Table + expect( + binUtils.outputFormatter({ + type: 'table', + data: [ + { key1: 'value1', key2: 'value2' }, + { key1: 'data1', key2: 'data2' }, + { key1: null, key2: undefined }, + ], + }), + ).toBe('key1\tkey2\nvalue1\tvalue2\ndata1\tdata2\n\t\n'); + // JSON + expect( + binUtils.outputFormatter({ + type: 'json', + data: [ + { key1: 'value1', key2: 'value2' }, + { key1: 'data1', key2: 'data2' }, + ], + }), + ).toBe( + '[{"key1":"value1","key2":"value2"},{"key1":"data1","key2":"data2"}]\n', + ); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'dict in human and in json format', + () => { + // Dict + expect( + binUtils.outputFormatter({ + type: 'dict', + data: { key1: 'value1', key2: 'value2' }, + }), + ).toBe('key1\t"value1"\nkey2\t"value2"\n'); + expect( + binUtils.outputFormatter({ + type: 'dict', + data: { key1: 'first\nsecond', key2: 'first\nsecond\n' }, + }), + ).toBe('key1\t"first\\nsecond"\nkey2\t"first\\nsecond\\n"\n'); + expect( + binUtils.outputFormatter({ + type: 'dict', + data: { key1: null, key2: undefined }, + }), + ).toBe('key1\t""\nkey2\t""\n'); + // JSON + expect( + binUtils.outputFormatter({ + type: 'json', + data: { key1: 'value1', key2: 'value2' }, + }), + ).toBe('{"key1":"value1","key2":"value2"}\n'); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'errors in human and json format', + () => { + const timestamp = new Date(); + const data = { string: 'one', number: 1 }; + const host = '127.0.0.1' as Host; + const port = 55555 as Port; + const nodeId = testUtils.generateRandomNodeId(); + const standardError = new TypeError('some error'); + const pkError = new ErrorPolykey('some pk error', { + timestamp, + data, + }); + const remoteError = new rpcErrors.ErrorPolykeyRemote( + { + nodeId: nodesUtils.encodeNodeId(nodeId), + host, + port, + command: 'some command', + }, + 'some remote error', + { timestamp, cause: pkError }, + ); + const twoRemoteErrors = new rpcErrors.ErrorPolykeyRemote( + { + nodeId: nodesUtils.encodeNodeId(nodeId), + host, + port, + command: 'command 2', + }, + 'remote error', + { + timestamp, + cause: new rpcErrors.ErrorPolykeyRemote( + { + nodeId: nodesUtils.encodeNodeId(nodeId), + host, + port, + command: 'command 1', + }, + undefined, + { + timestamp, + cause: new ErrorPolykey('pk error', { + timestamp, + cause: standardError, + }), + }, + ), + }, + ); + // Human + expect( + binUtils.outputFormatter({ type: 'error', data: standardError }), + ).toBe(`${standardError.name}: ${standardError.message}\n`); + expect(binUtils.outputFormatter({ type: 'error', data: pkError })).toBe( + `${pkError.name}: ${pkError.description} - ${pkError.message}\n` + + ` data\t${JSON.stringify(data)}\n`, + ); + expect( + binUtils.outputFormatter({ type: 'error', data: remoteError }), + ).toBe( + `${remoteError.name}: ${remoteError.description} - ${remoteError.message}\n` + + ` nodeId\t${nodesUtils.encodeNodeId(nodeId)}\n` + + ` host\t${host}\n` + + ` port\t${port}\n` + + ` command\tsome command\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` cause: ${remoteError.cause.name}: ${remoteError.cause.description} - ${remoteError.cause.message}\n` + + ` data\t${JSON.stringify(data)}\n`, + ); + expect( + binUtils.outputFormatter({ type: 'error', data: twoRemoteErrors }), + ).toBe( + `${twoRemoteErrors.name}: ${twoRemoteErrors.description} - ${twoRemoteErrors.message}\n` + + ` nodeId\t${nodesUtils.encodeNodeId(nodeId)}\n` + + ` host\t${host}\n` + + ` port\t${port}\n` + + ` command\tcommand 2\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` cause: ${twoRemoteErrors.cause.name}: ${twoRemoteErrors.cause.description}\n` + + ` nodeId\t${nodesUtils.encodeNodeId(nodeId)}\n` + + ` host\t${host}\n` + + ` port\t${port}\n` + + ` command\t${twoRemoteErrors.cause.metadata.command}\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` cause: ${twoRemoteErrors.cause.cause.name}: ${twoRemoteErrors.cause.cause.description} - ${twoRemoteErrors.cause.cause.message}\n` + + ` cause: ${standardError.name}: ${standardError.message}\n`, + ); + // JSON + expect( + binUtils.outputFormatter({ type: 'json', data: standardError }), + ).toBe( + `{"type":"${standardError.name}","data":{"message":"${ + standardError.message + }","stack":"${standardError.stack?.replaceAll('\n', '\\n')}"}}\n`, + ); + expect(binUtils.outputFormatter({ type: 'json', data: pkError })).toBe( + JSON.stringify(pkError.toJSON()) + '\n', + ); + expect( + binUtils.outputFormatter({ type: 'json', data: remoteError }), + ).toBe(JSON.stringify(remoteError.toJSON()) + '\n'); + expect( + binUtils.outputFormatter({ type: 'json', data: twoRemoteErrors }), + ).toBe(JSON.stringify(twoRemoteErrors.toJSON()) + '\n'); + }, + ); +}); diff --git a/tests/utils/exec.ts b/tests/utils/exec.ts new file mode 100644 index 00000000..08959fee --- /dev/null +++ b/tests/utils/exec.ts @@ -0,0 +1,601 @@ +import type { ChildProcess } from 'child_process'; +import type ErrorPolykey from 'polykey/dist/ErrorPolykey'; +import childProcess from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import process from 'process'; +import readline from 'readline'; +import * as mockProcess from 'jest-mock-process'; +import mockedEnv from 'mocked-env'; +import nexpect from 'nexpect'; +import Logger from '@matrixai/logger'; +import main from '@/polykey'; + +type ExecOpts = { + env: Record; + command?: string | undefined; + cwd?: string; + shell?: boolean; +}; + +const tsConfigPath = path.resolve( + path.join(globalThis.projectDir ?? '', 'tsconfig.json'), +); + +const polykeyPath = path.resolve( + path.join(globalThis.projectDir ?? '', 'src/polykey.ts'), +); + +const generateDockerArgs = (mountPath: string) => [ + '--interactive', + '--rm', + '--network', + 'host', + '--pid', + 'host', + '--userns', + 'host', + `--user`, + `${process.getuid!()}`, + '--mount', + `type=bind,src=${mountPath},dst=${mountPath}`, + '--env', + 'PK_PASSWORD', + '--env', + 'PK_NODE_PATH', + '--env', + 'PK_RECOVERY_CODE', + '--env', + 'PK_TOKEN', + '--env', + 'PK_ROOT_KEY', + '--env', + 'PK_NODE_ID', + '--env', + 'PK_CLIENT_HOST', + '--env', + 'PK_CLIENT_PORT', +]; + +/** + * Execute generic (non-Polykey) shell commands + */ +async function exec( + command: string, + args: Array = [], + opts: ExecOpts = { env: {} }, +): Promise<{ + exitCode: number; + stdout: string; + stderr: string; +}> { + const env = { + ...process.env, + ...opts.env, + }; + return new Promise((resolve, reject) => { + let stdout = '', + stderr = ''; + const subprocess = childProcess.spawn(command, args, { + env, + windowsHide: true, + shell: opts.shell ? opts.shell : false, + }); + subprocess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + subprocess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + subprocess.on('exit', (code) => { + resolve({ exitCode: code ?? -255, stdout, stderr }); + }); + subprocess.on('error', (e) => { + reject(e); + }); + }); +} + +/** + * Spawn generic (non-Polykey) shell processes + */ +async function spawn( + command: string, + args: Array = [], + opts: ExecOpts = { env: {} }, + logger: Logger = new Logger(spawn.name), +): Promise { + const env = { + ...process.env, + ...opts.env, + }; + const subprocess = childProcess.spawn(command, args, { + env, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + shell: opts.shell ? opts.shell : false, + }); + // The readline library will trim newlines + const rlOut = readline.createInterface(subprocess.stdout!); + rlOut.on('line', (l) => logger.info(l)); + const rlErr = readline.createInterface(subprocess.stderr!); + rlErr.on('line', (l) => logger.info(l)); + return new Promise((resolve, reject) => { + subprocess.on('error', (e) => { + reject(e); + }); + subprocess.on('spawn', () => { + subprocess.removeAllListeners('error'); + resolve(subprocess); + }); + }); +} + +/** + * Runs pk command functionally + */ +async function pk(args: Array): Promise { + return main(['', '', ...args]); +} + +/** + * Runs pk command functionally with mocked STDIO + * Both stdout and stderr are the entire output including newlines + * This can only be used serially, because the mocks it relies on are global singletons + * If it is used concurrently, the mocking side-effects can conflict + */ +async function pkStdio( + args: Array = [], + opts: ExecOpts = { env: {} }, +): Promise<{ + exitCode: number; + stdout: string; + stderr: string; +}> { + const cwd = + opts.cwd ?? + (await fs.promises.mkdtemp(path.join(globalThis.tmpDir, 'polykey-test-'))); + // Recall that we attempt to connect to all specified seed nodes on agent start. + // Therefore, for testing purposes only, we default the seed nodes as empty + // (if not defined in the env) to ensure no attempted connections. A regular + // PolykeyAgent is expected to initially connect to the mainnet seed nodes + opts.env['PK_SEED_NODES'] = opts.env['PK_SEED_NODES'] ?? ''; + // Parse the arguments of process.stdout.write and process.stderr.write + const parseArgs = (args) => { + const data = args[0]; + if (typeof data === 'string') { + return data; + } else { + let encoding: BufferEncoding = 'utf8'; + if (typeof args[1] === 'string') { + encoding = args[1] as BufferEncoding; + } + const buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + return buffer.toString(encoding); + } + }; + // Process events are not allowed when testing + const mockProcessOn = mockProcess.spyOnImplementing( + process, + 'on', + () => process, + ); + const mockProcessOnce = mockProcess.spyOnImplementing( + process, + 'once', + () => process, + ); + const mockProcessAddListener = mockProcess.spyOnImplementing( + process, + 'addListener', + () => process, + ); + const mockProcessOff = mockProcess.spyOnImplementing( + process, + 'off', + () => process, + ); + const mockProcessRemoveListener = mockProcess.spyOnImplementing( + process, + 'removeListener', + () => process, + ); + const mockCwd = mockProcess.spyOnImplementing(process, 'cwd', () => cwd!); + const envRestore = mockedEnv(opts.env); + const mockedStdout = mockProcess.mockProcessStdout(); + const mockedStderr = mockProcess.mockProcessStderr(); + const exitCode = await pk(args); + // Calls is an array of parameter arrays + // Only the first parameter is the string written + const stdout = mockedStdout.mock.calls.map(parseArgs).join(''); + const stderr = mockedStderr.mock.calls.map(parseArgs).join(''); + mockedStderr.mockRestore(); + mockedStdout.mockRestore(); + envRestore(); + mockCwd.mockRestore(); + mockProcessRemoveListener.mockRestore(); + mockProcessOff.mockRestore(); + mockProcessAddListener.mockRestore(); + mockProcessOnce.mockRestore(); + mockProcessOn.mockRestore(); + return { + exitCode, + stdout, + stderr, + }; +} + +/** + * Runs pk command through subprocess + * This is used when a subprocess functionality needs to be used + * This is intended for terminating subprocesses + * Both stdout and stderr are the entire output including newlines + * By default `globalThis.testCommand` should be `undefined` because `PK_TEST_COMMAND` will not be set + * This is strictly checking for existence, `PK_TEST_COMMAND=''` is legitimate but undefined behaviour + */ +async function pkExec( + args: Array = [], + opts: ExecOpts = { env: {}, command: globalThis.testCmd }, +): Promise<{ + exitCode: number; + stdout: string; + stderr: string; +}> { + if (opts.command == null) { + return pkExecWithoutShell(args, opts); + } else { + return pkExecWithShell(args, opts); + } +} + +/** + * Launch pk command through subprocess + * This is used when a subprocess functionality needs to be used + * This is intended for non-terminating subprocesses + * By default `globalThis.testCommand` should be `undefined` because `PK_TEST_COMMAND` will not be set + * This is strictly checking for existence, `PK_TEST_COMMAND=''` is legitimate but undefined behaviour + */ +async function pkSpawn( + args: Array = [], + opts: ExecOpts = { env: {}, command: globalThis.testCmd }, + logger: Logger = new Logger(pkSpawn.name), +): Promise { + if (opts.command == null) { + return pkSpawnWithoutShell(args, opts, logger); + } else { + return pkSpawnWithShell(args, opts, logger); + } +} + +/** + * Runs pk command through subprocess + * This is the default + */ +async function pkExecWithoutShell( + args: Array = [], + opts: ExecOpts = { env: {} }, +): Promise<{ + exitCode: number; + stdout: string; + stderr: string; +}> { + const cwd = + opts.cwd ?? + (await fs.promises.mkdtemp(path.join(globalThis.tmpDir, 'polykey-test-'))); + const env = { + ...process.env, + ...opts.env, + }; + // Recall that we attempt to connect to all specified seed nodes on agent start. + // Therefore, for testing purposes only, we default the seed nodes as empty + // (if not defined in the env) to ensure no attempted connections. A regular + // PolykeyAgent is expected to initially connect to the mainnet seed nodes + env['PK_SEED_NODES'] = env['PK_SEED_NODES'] ?? ''; + return new Promise((resolve, reject) => { + let stdout = '', + stderr = ''; + const subprocess = childProcess.spawn( + 'ts-node', + ['--project', tsConfigPath, polykeyPath, ...args], + { + env, + cwd, + windowsHide: true, + shell: opts.shell ? opts.shell : false, + }, + ); + subprocess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + subprocess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + subprocess.on('exit', (code) => { + resolve({ exitCode: code ?? -255, stdout, stderr }); + }); + subprocess.on('error', (e) => { + reject(e); + }); + }); +} + +/** + * Runs pk command through subprocess + * This is the parameter > environment override + */ +async function pkExecWithShell( + args: Array = [], + opts: ExecOpts = { env: {}, command: globalThis.testCmd }, +): Promise<{ + exitCode: number; + stdout: string; + stderr: string; +}> { + const cwd = path.resolve( + opts.cwd ?? + (await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + )), + ); + const env = { + ...process.env, + ...opts.env, + }; + if (globalThis.testPlatform === 'docker') { + env.DOCKER_OPTIONS = generateDockerArgs(cwd).join(' '); + } + // Recall that we attempt to connect to all specified seed nodes on agent start. + // Therefore, for testing purposes only, we default the seed nodes as empty + // (if not defined in the env) to ensure no attempted connections. A regular + // PolykeyAgent is expected to initially connect to the mainnet seed nodes + env['PK_SEED_NODES'] = env['PK_SEED_NODES'] ?? ''; + args = args.map(escapeShellArgs); + return new Promise((resolve, reject) => { + let stdout = '', + stderr = ''; + const subprocess = childProcess.spawn(opts.command!, args, { + env, + cwd, + windowsHide: true, + shell: opts.shell ? opts.shell : true, + }); + subprocess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + subprocess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + subprocess.on('exit', (code) => { + resolve({ exitCode: code ?? -255, stdout, stderr }); + }); + subprocess.on('error', (e) => { + reject(e); + }); + }); +} + +/** + * Launch pk command through subprocess + * This is the default + */ +async function pkSpawnWithoutShell( + args: Array = [], + opts: ExecOpts = { env: {} }, + logger: Logger = new Logger(pkSpawnWithoutShell.name), +): Promise { + const cwd = + opts.cwd ?? + (await fs.promises.mkdtemp(path.join(globalThis.tmpDir, 'polykey-test-'))); + const env = { + ...process.env, + ...opts.env, + }; + // Recall that we attempt to connect to all specified seed nodes on agent start. + // Therefore, for testing purposes only, we default the seed nodes as empty + // (if not defined in the env) to ensure no attempted connections. A regular + // PolykeyAgent is expected to initially connect to the mainnet seed nodes + env['PK_SEED_NODES'] = env['PK_SEED_NODES'] ?? ''; + const subprocess = childProcess.spawn( + 'ts-node', + ['--project', tsConfigPath, polykeyPath, ...args], + { + env, + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + shell: opts.shell ? opts.shell : false, + }, + ); + // The readline library will trim newlines + const rlOut = readline.createInterface(subprocess.stdout!); + rlOut.on('line', (l) => logger.info(l)); + const rlErr = readline.createInterface(subprocess.stderr!); + rlErr.on('line', (l) => logger.info(l)); + return new Promise((resolve, reject) => { + subprocess.on('error', (e) => { + reject(e); + }); + subprocess.on('spawn', () => { + subprocess.removeAllListeners('error'); + resolve(subprocess); + }); + }); +} + +/** + * Launch pk command through subprocess + * This is the parameter > environment override + */ +async function pkSpawnWithShell( + args: Array = [], + opts: ExecOpts = { env: {}, command: globalThis.testCmd }, + logger: Logger = new Logger(pkSpawnWithShell.name), +): Promise { + const cwd = path.resolve( + opts.cwd ?? + (await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + )), + ); + const env = { + ...process.env, + ...opts.env, + }; + if (globalThis.testPlatform === 'docker') { + env.DOCKER_OPTIONS = generateDockerArgs(cwd).join(' '); + } + // Recall that we attempt to connect to all specified seed nodes on agent start. + // Therefore, for testing purposes only, we default the seed nodes as empty + // (if not defined in the env) to ensure no attempted connections. A regular + // PolykeyAgent is expected to initially connect to the mainnet seed nodes + env['PK_SEED_NODES'] = env['PK_SEED_NODES'] ?? ''; + args = args.map(escapeShellArgs); + const subprocess = childProcess.spawn(opts.command!, args, { + env, + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + shell: opts.shell ? opts.shell : true, + }); + // The readline library will trim newlines + const rlOut = readline.createInterface(subprocess.stdout!); + rlOut.on('line', (l) => logger.info(l)); + const rlErr = readline.createInterface(subprocess.stderr!); + rlErr.on('line', (l) => logger.info(l)); + return new Promise((resolve, reject) => { + subprocess.on('error', (e) => { + reject(e); + }); + subprocess.on('spawn', () => { + subprocess.removeAllListeners('error'); + resolve(subprocess); + }); + }); +} + +/** + * Runs pk command through subprocess expect wrapper + * Note this will eventually be refactored to follow the same pattern as + * `pkExec` and `pkSpawn` using a workaround to inject the `shell` option + * into `nexpect.spawn` + * @throws assert.AssertionError when expectations fail + * @throws Error for other reasons + */ +async function pkExpect({ + expect, + args = [], + env = {}, + cwd, +}: { + expect: (expectChain: nexpect.IChain) => nexpect.IChain; + args?: Array; + env?: Record; + cwd?: string; +}): Promise<{ + exitCode: number; + stdouterr: string; +}> { + cwd = + cwd ?? + (await fs.promises.mkdtemp(path.join(globalThis.tmpDir, 'polykey-test-'))); + env = { + ...process.env, + ...env, + }; + // Recall that we attempt to connect to all specified seed nodes on agent start. + // Therefore, for testing purposes only, we default the seed nodes as empty + // (if not defined in the env) to ensure no attempted connections. A regular + // PolykeyAgent is expected to initially connect to the mainnet seed nodes + env['PK_SEED_NODES'] = env['PK_SEED_NODES'] ?? ''; + // Expect chain runs against stdout and stderr + let expectChain = nexpect.spawn( + 'ts-node', + ['--project', tsConfigPath, polykeyPath, ...args], + { + env, + cwd, + stream: 'all', + }, + ); + // Augment the expect chain + expectChain = expect(expectChain); + return new Promise((resolve, reject) => { + expectChain.run((e, output: Array, exitCode: string | number) => { + if (e != null) { + return reject(e); + } + if (typeof exitCode === 'string') { + return reject(new Error('Process killed by signal')); + } + const stdouterr = output.join('\n'); + return resolve({ + stdouterr, + exitCode, + }); + }); + }); +} + +/** + * Waits for child process to exit + * When process is terminated with signal + * The code will be null + * When the process exits by itself, the signal will be null + */ +async function processExit( + process: ChildProcess, +): Promise<[number | null, NodeJS.Signals | null]> { + return await new Promise((resolve) => { + process.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); +} + +/** + * Checks exit code and stderr against ErrorPolykey + * Errors should contain all of the errors in the expected error chain + * starting with the outermost error (excluding ErrorPolykeyRemote) + * When using this function, the command must be run with --format=json + */ +function expectProcessError( + exitCode: number, + stderr: string, + errors: Array>, +) { + expect(exitCode).toBe(errors[0].exitCode); + const stdErrLine = stderr.trim().split('\n').pop(); + let currentError = JSON.parse(stdErrLine!); + while (currentError.type === 'ErrorPolykeyRemote') { + if (currentError.data.cause == null) throw Error('No cause was given'); + currentError = currentError.data.cause; + } + for (const error of errors) { + expect(currentError.type).toBe(error.name); + expect(currentError.data.message).toBe(error.message); + currentError = currentError.data.cause; + } +} + +function escapeShellArgs(arg: string): string { + return arg.replace(/(["\s'$`\\])/g, '\\$1'); +} + +export { + tsConfigPath, + polykeyPath, + exec, + spawn, + pk, + pkStdio, + pkExec, + pkExecWithShell, + pkExecWithoutShell, + pkSpawn, + pkSpawnWithShell, + pkSpawnWithoutShell, + pkExpect, + processExit, + expectProcessError, + escapeShellArgs, +}; diff --git a/tests/utils/index.ts b/tests/utils/index.ts new file mode 100644 index 00000000..e8ac84a3 --- /dev/null +++ b/tests/utils/index.ts @@ -0,0 +1,4 @@ +export * from './exec'; +export * from './platform'; +export * from './testAgent'; +export * from './utils'; diff --git a/tests/utils/platform.ts b/tests/utils/platform.ts new file mode 100644 index 00000000..515c0659 --- /dev/null +++ b/tests/utils/platform.ts @@ -0,0 +1,35 @@ +import shell from 'shelljs'; + +/** + * The `isTestPlatformX` constants are temporary until #435 is resolved + */ + +const isTestPlatformLinux = globalThis.testPlatform === 'linux'; +const isTestPlatformMacOs = globalThis.testPlatform === 'macos'; +const isTestPlatformWindows = globalThis.testPlatform === 'windows'; +const isTestPlatformDocker = globalThis.testPlatform === 'docker'; +const isTestPlatformEmpty = globalThis.testPlatform == null; + +const isPlatformLinux = process.platform === 'linux'; +const isPlatformWin32 = process.platform === 'win32'; +const isPlatformDarwin = process.platform === 'darwin'; + +const hasIp = shell.which('ip'); +const hasIptables = shell.which('iptables'); +const hasNsenter = shell.which('nsenter'); +const hasUnshare = shell.which('unshare'); + +export { + isTestPlatformLinux, + isTestPlatformMacOs, + isTestPlatformWindows, + isTestPlatformDocker, + isTestPlatformEmpty, + isPlatformLinux, + isPlatformWin32, + isPlatformDarwin, + hasIp, + hasIptables, + hasNsenter, + hasUnshare, +}; diff --git a/tests/utils/testAgent.ts b/tests/utils/testAgent.ts new file mode 100644 index 00000000..55580464 --- /dev/null +++ b/tests/utils/testAgent.ts @@ -0,0 +1,76 @@ +import type { StatusLive } from 'polykey/dist/status/types'; +import type Logger from '@matrixai/logger'; +import fs from 'fs'; +import path from 'path'; +import readline from 'readline'; +import * as utils from 'polykey/dist/utils/utils'; +import * as validationUtils from 'polykey/dist/validation/utils'; +import * as execUtils from './exec'; + +async function setupTestAgent(logger: Logger) { + const agentDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + const agentPassword = 'password'; + const agentProcess = await execUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + agentDir, + '--client-host', + '127.0.0.1', + '--agent-host', + '127.0.0.1', + '--workers', + 'none', + '--format', + 'json', + '--verbose', + ], + { + env: { + PK_PASSWORD: agentPassword, + PK_PASSWORD_OPS_LIMIT: 'min', + PK_PASSWORD_MEM_LIMIT: 'min', + }, + cwd: agentDir, + command: globalThis.testCmd, + }, + logger, + ); + const startedProm = utils.promise(); + agentProcess.on('error', (d) => startedProm.rejectP(d)); + const rlOut = readline.createInterface(agentProcess.stdout!); + rlOut.on('line', (l) => startedProm.resolveP(JSON.parse(l.toString()))); + const data = await startedProm.p; + const agentStatus: StatusLive = { + status: 'LIVE', + data: { ...data, nodeId: validationUtils.parseNodeId(data.nodeId) }, + }; + try { + return { + agentStatus, + agentClose: async () => { + agentProcess.kill(); + await fs.promises.rm(agentDir, { + recursive: true, + force: true, + maxRetries: 10, + }); + }, + agentDir, + agentPassword, + }; + } catch (e) { + agentProcess.kill(); + await fs.promises.rm(agentDir, { + recursive: true, + force: true, + maxRetries: 10, + }); + throw e; + } +} + +export { setupTestAgent }; diff --git a/tests/utils/utils.ts b/tests/utils/utils.ts new file mode 100644 index 00000000..4a4a99ee --- /dev/null +++ b/tests/utils/utils.ts @@ -0,0 +1,89 @@ +import type { NodeId } from 'polykey/dist/ids/types'; +import type PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import type { Host, Port } from 'polykey/dist/network/types'; +import type { NodeAddress } from 'polykey/dist/nodes/types'; +import { IdInternal } from '@matrixai/id'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import { promise } from 'polykey/dist/utils/utils'; + +function generateRandomNodeId(): NodeId { + const random = keysUtils.getRandomBytes(16).toString('hex'); + return IdInternal.fromString(random); +} + +function testIf(condition: boolean) { + return condition ? test : test.skip; +} + +function describeIf(condition: boolean) { + return condition ? describe : describe.skip; +} + +function trackTimers() { + const timerMap: Map = new Map(); + const oldClearTimeout = globalThis.clearTimeout; + const newClearTimeout = (...args) => { + timerMap.delete(args[0]); + // @ts-ignore: slight type mismatch + oldClearTimeout(...args); + }; + globalThis.clearTimeout = newClearTimeout; + + const oldSetTimeout = globalThis.setTimeout; + const newSetTimeout = (handler: TimerHandler, timeout?: number) => { + const prom = promise(); + const stack = Error(); + const newCallback = async (...args) => { + // @ts-ignore: only expecting functions + await handler(...args); + prom.resolveP(); + }; + const result = oldSetTimeout(newCallback, timeout); + timerMap.set(result, { timeout, stack }); + void prom.p.finally(() => { + timerMap.delete(result); + }); + return result; + }; + // @ts-ignore: slight type mismatch + globalThis.setTimeout = newSetTimeout; + + // Setting up interval + const oldSetInterval = globalThis.setInterval; + const newSetInterval = (...args) => { + // @ts-ignore: slight type mismatch + const result = oldSetInterval(...args); + timerMap.set(result, { timeout: args[0], error: Error() }); + return result; + }; + // @ts-ignore: slight type mismatch + globalThis.setInterval = newSetInterval; + + const oldClearInterval = globalThis.clearInterval; + const newClearInterval = (timer) => { + timerMap.delete(timer); + return oldClearInterval(timer); + }; + // @ts-ignore: slight type mismatch + globalThis.clearInterval = newClearInterval(); + + return timerMap; +} + +/** + * Adds each node's details to the other + */ +async function nodesConnect(localNode: PolykeyAgent, remoteNode: PolykeyAgent) { + // Add remote node's details to local node + await localNode.nodeManager.setNode(remoteNode.keyRing.getNodeId(), { + host: remoteNode.quicServerAgent.host as unknown as Host, + port: remoteNode.quicServerAgent.port as unknown as Port, + } as NodeAddress); + // Add local node's details to remote node + await remoteNode.nodeManager.setNode(localNode.keyRing.getNodeId(), { + host: localNode.quicServerAgent.host as unknown as Host, + port: localNode.quicServerAgent.port as unknown as Port, + } as NodeAddress); +} + +export { generateRandomNodeId, testIf, describeIf, trackTimers, nodesConnect }; diff --git a/tests/vaults/vaults.test.ts b/tests/vaults/vaults.test.ts new file mode 100644 index 00000000..fde4d693 --- /dev/null +++ b/tests/vaults/vaults.test.ts @@ -0,0 +1,929 @@ +import type { NodeAddress } from 'polykey/dist/nodes/types'; +import type { VaultId, VaultName } from 'polykey/dist/vaults/types'; +import type { Host, Port } from 'polykey/dist/network/types'; +import type { GestaltNodeInfo } from 'polykey/dist/gestalts/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as vaultsUtils from 'polykey/dist/vaults/utils'; +import sysexits from 'polykey/dist/utils/sysexits'; +import NotificationsManager from 'polykey/dist/notifications/NotificationsManager'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from '../utils'; + +describe('CLI vaults', () => { + const password = 'password'; + const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]); + let dataDir: string; + let passwordFile: string; + let polykeyAgent: PolykeyAgent; + let command: Array; + let vaultNumber: number; + let vaultName: VaultName; + + const nodeId1 = testUtils.generateRandomNodeId(); + const nodeId2 = testUtils.generateRandomNodeId(); + const nodeId3 = testUtils.generateRandomNodeId(); + + const node1: GestaltNodeInfo = { + nodeId: nodeId1, + }; + const node2: GestaltNodeInfo = { + nodeId: nodeId2, + }; + const node3: GestaltNodeInfo = { + nodeId: nodeId3, + }; + + // Helper functions + function genVaultName() { + vaultNumber++; + return `vault-${vaultNumber}` as VaultName; + } + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + passwordFile = path.join(dataDir, 'passwordFile'); + await fs.promises.writeFile(passwordFile, 'password'); + polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: dataDir, + logger: logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + await polykeyAgent.gestaltGraph.setNode(node1); + await polykeyAgent.gestaltGraph.setNode(node2); + await polykeyAgent.gestaltGraph.setNode(node3); + + vaultNumber = 0; + + // Authorize session + await testUtils.pkStdio( + ['agent', 'unlock', '-np', dataDir, '--password-file', passwordFile], + { + env: {}, + cwd: dataDir, + }, + ); + vaultName = genVaultName(); + command = []; + }); + afterEach(async () => { + await polykeyAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + describe('commandListVaults', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should list all vaults', + async () => { + command = ['vaults', 'list', '-np', dataDir]; + await polykeyAgent.vaultManager.createVault('Vault1' as VaultName); + await polykeyAgent.vaultManager.createVault('Vault2' as VaultName); + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + }, + ); + }); + describe('commandCreateVaults', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should create vaults', + async () => { + command = ['vaults', 'create', '-np', dataDir, 'MyTestVault']; + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + const result2 = await testUtils.pkStdio( + ['vaults', 'touch', '-np', dataDir, 'MyTestVault2'], + { + env: {}, + cwd: dataDir, + }, + ); + expect(result2.exitCode).toBe(0); + + const list = (await polykeyAgent.vaultManager.listVaults()).keys(); + const namesList: string[] = []; + for await (const name of list) { + namesList.push(name); + } + expect(namesList).toContain('MyTestVault'); + expect(namesList).toContain('MyTestVault2'); + }, + ); + }); + describe('commandRenameVault', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should rename vault', + async () => { + command = [ + 'vaults', + 'rename', + vaultName, + 'RenamedVault', + '-np', + dataDir, + ]; + await polykeyAgent.vaultManager.createVault(vaultName); + const id = polykeyAgent.vaultManager.getVaultId(vaultName); + expect(id).toBeTruthy(); + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + const list = (await polykeyAgent.vaultManager.listVaults()).keys(); + const namesList: string[] = []; + for await (const name of list) { + namesList.push(name); + } + expect(namesList).toContain('RenamedVault'); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should fail to rename non-existent vault', + async () => { + command = [ + 'vaults', + 'rename', + 'z4iAXFwgHGeyUrdC5CiCNU4', // Vault does not exist + 'RenamedVault', + '-np', + dataDir, + ]; + await polykeyAgent.vaultManager.createVault(vaultName); + const id = polykeyAgent.vaultManager.getVaultId(vaultName); + expect(id).toBeTruthy(); + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + // Exit code of the exception + expect(result.exitCode).toBe(sysexits.USAGE); + + const list = (await polykeyAgent.vaultManager.listVaults()).keys(); + const namesList: string[] = []; + for await (const name of list) { + namesList.push(name); + } + expect(namesList).toContain(vaultName); + }, + ); + }); + describe('commandDeleteVault', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should delete vault', + async () => { + command = ['vaults', 'delete', '-np', dataDir, vaultName]; + await polykeyAgent.vaultManager.createVault(vaultName); + let id = polykeyAgent.vaultManager.getVaultId(vaultName); + expect(id).toBeTruthy(); + + id = polykeyAgent.vaultManager.getVaultId(vaultName); + expect(id).toBeTruthy(); + + const result2 = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result2.exitCode).toBe(0); + + const list = (await polykeyAgent.vaultManager.listVaults()).keys(); + const namesList: string[] = []; + for await (const name of list) { + namesList.push(name); + } + expect(namesList).not.toContain(vaultName); + }, + ); + }); + // TODO: disable until agent migration stage 2 is done and vault cloning works again. + testUtils.testIf(testUtils.isTestPlatformEmpty && false)( + 'should clone and pull a vault', + async () => { + const dataDir2 = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + const targetPolykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath: dataDir2, + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + logger: logger, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + const vaultId = await targetPolykeyAgent.vaultManager.createVault( + vaultName, + ); + await targetPolykeyAgent.vaultManager.withVaults( + [vaultId], + async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile('secret 1', 'secret the first'); + }); + }, + ); + + await targetPolykeyAgent.gestaltGraph.setNode({ + nodeId: polykeyAgent.keyRing.getNodeId(), + }); + const targetNodeId = targetPolykeyAgent.keyRing.getNodeId(); + const targetNodeIdEncoded = nodesUtils.encodeNodeId(targetNodeId); + await polykeyAgent.nodeManager.setNode(targetNodeId, { + host: targetPolykeyAgent.quicServerAgent.host as unknown as Host, + port: targetPolykeyAgent.quicServerAgent.port as unknown as Port, + }); + await targetPolykeyAgent.nodeManager.setNode( + polykeyAgent.keyRing.getNodeId(), + { + host: polykeyAgent.quicServerAgent.host as unknown as Host, + port: polykeyAgent.quicServerAgent.port as unknown as Port, + }, + ); + await polykeyAgent.acl.setNodePerm(targetNodeId, { + gestalt: { + notify: null, + }, + vaults: {}, + }); + + const nodeId = polykeyAgent.keyRing.getNodeId(); + await targetPolykeyAgent.gestaltGraph.setGestaltAction( + ['node', nodeId], + 'scan', + ); + await targetPolykeyAgent.acl.setVaultAction(vaultId, nodeId, 'clone'); + await targetPolykeyAgent.acl.setVaultAction(vaultId, nodeId, 'pull'); + + command = [ + 'vaults', + 'clone', + '-np', + dataDir, + vaultsUtils.encodeVaultId(vaultId), + targetNodeIdEncoded, + ]; + + let result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + const clonedVaultId = await polykeyAgent.vaultManager.getVaultId( + vaultName, + ); + + await polykeyAgent.vaultManager.withVaults( + [clonedVaultId!], + async (clonedVault) => { + const file = await clonedVault.readF(async (efs) => { + return await efs.readFile('secret 1', { encoding: 'utf8' }); + }); + expect(file).toBe('secret the first'); + }, + ); + + await polykeyAgent.vaultManager.destroyVault(clonedVaultId!); + command = [ + 'vaults', + 'clone', + '-np', + dataDir, + vaultName, + nodesUtils.encodeNodeId(targetNodeId), + ]; + result = await testUtils.pkStdio([...command], { env: {}, cwd: dataDir }); + expect(result.exitCode).toBe(0); + + const secondClonedVaultId = (await polykeyAgent.vaultManager.getVaultId( + vaultName, + ))!; + await polykeyAgent.vaultManager.withVaults( + [secondClonedVaultId!], + async (secondClonedVault) => { + const file = await secondClonedVault.readF(async (efs) => { + return await efs.readFile('secret 1', { encoding: 'utf8' }); + }); + expect(file).toBe('secret the first'); + }, + ); + + await targetPolykeyAgent.vaultManager.withVaults( + [vaultId], + async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile('secret 2', 'secret the second'); + }); + }, + ); + + command = ['vaults', 'pull', '-np', dataDir, vaultName]; + result = await testUtils.pkStdio([...command], { env: {}, cwd: dataDir }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults( + [secondClonedVaultId!], + async (secondClonedVault) => { + const file = await secondClonedVault.readF(async (efs) => { + return await efs.readFile('secret 2', { encoding: 'utf8' }); + }); + expect(file).toBe('secret the second'); + }, + ); + + command = [ + 'vaults', + 'pull', + '-np', + dataDir, + '-pv', + 'InvalidName', + vaultsUtils.encodeVaultId(secondClonedVaultId), + targetNodeIdEncoded, + ]; + result = await testUtils.pkStdio([...command], { env: {}, cwd: dataDir }); + expect(result.exitCode).toBe(sysexits.USAGE); + expect(result.stderr).toContain('ErrorVaultsVaultUndefined'); + + command = [ + 'vaults', + 'pull', + '-np', + dataDir, + '-pv', + vaultName, + vaultsUtils.encodeVaultId(secondClonedVaultId), + 'InvalidNodeId', + ]; + result = await testUtils.pkStdio([...command], { env: {}, cwd: dataDir }); + expect(result.exitCode).toBe(sysexits.USAGE); + + await targetPolykeyAgent.stop(); + await fs.promises.rm(dataDir2, { + force: true, + recursive: true, + }); + }, + globalThis.defaultTimeout * 3, + ); + describe('commandShare', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'Should share a vault', + async () => { + const mockedSendNotification = jest.spyOn( + NotificationsManager.prototype, + 'sendNotification', + ); + try { + // We don't want to actually send a notification + mockedSendNotification.mockImplementation(async (_) => {}); + const vaultId = await polykeyAgent.vaultManager.createVault( + vaultName, + ); + const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); + const targetNodeId = testUtils.generateRandomNodeId(); + const targetNodeIdEncoded = nodesUtils.encodeNodeId(targetNodeId); + await polykeyAgent.gestaltGraph.setNode({ + nodeId: targetNodeId, + }); + expect( + (await polykeyAgent.acl.getNodePerm(targetNodeId))?.vaults[vaultId], + ).toBeUndefined(); + + command = [ + 'vaults', + 'share', + '-np', + dataDir, + vaultIdEncoded, + targetNodeIdEncoded, + ]; + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + // Check permission + const permissions1 = ( + await polykeyAgent.acl.getNodePerm(targetNodeId) + )?.vaults[vaultId]; + expect(permissions1).toBeDefined(); + expect(permissions1.pull).toBeDefined(); + expect(permissions1.clone).toBeDefined(); + } finally { + mockedSendNotification.mockRestore(); + } + }, + ); + }); + describe('commandUnshare', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'Should unshare a vault', + async () => { + const vaultId1 = await polykeyAgent.vaultManager.createVault(vaultName); + const vaultId2 = await polykeyAgent.vaultManager.createVault( + vaultName + '1', + ); + const vaultIdEncoded1 = vaultsUtils.encodeVaultId(vaultId1); + const vaultIdEncoded2 = vaultsUtils.encodeVaultId(vaultId2); + const targetNodeId = testUtils.generateRandomNodeId(); + const targetNodeIdEncoded = nodesUtils.encodeNodeId(targetNodeId); + await polykeyAgent.gestaltGraph.setNode({ + nodeId: targetNodeId, + }); + + // Creating permissions + await polykeyAgent.gestaltGraph.setGestaltAction( + ['node', targetNodeId], + 'scan', + ); + await polykeyAgent.acl.setVaultAction(vaultId1, targetNodeId, 'clone'); + await polykeyAgent.acl.setVaultAction(vaultId1, targetNodeId, 'pull'); + await polykeyAgent.acl.setVaultAction(vaultId2, targetNodeId, 'clone'); + await polykeyAgent.acl.setVaultAction(vaultId2, targetNodeId, 'pull'); + + command = [ + 'vaults', + 'unshare', + '-np', + dataDir, + vaultIdEncoded1, + targetNodeIdEncoded, + ]; + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + // Check permission + const permissions = (await polykeyAgent.acl.getNodePerm(targetNodeId)) + ?.vaults[vaultId1]; + expect(permissions).toBeDefined(); + expect(permissions.pull).toBeUndefined(); + expect(permissions.clone).toBeUndefined(); + + expect( + (await polykeyAgent.acl.getNodePerm(targetNodeId))?.gestalt['scan'], + ).toBeDefined(); + + command = [ + 'vaults', + 'unshare', + '-np', + dataDir, + vaultIdEncoded2, + targetNodeIdEncoded, + ]; + const result2 = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result2.exitCode).toBe(0); + + // Check permission + const permissions2 = (await polykeyAgent.acl.getNodePerm(targetNodeId)) + ?.vaults[vaultId2]; + expect(permissions2).toBeDefined(); + expect(permissions2.pull).toBeUndefined(); + expect(permissions2.clone).toBeUndefined(); + + // And the scan permission should be removed + expect( + (await polykeyAgent.acl.getNodePerm(targetNodeId))?.gestalt['scan'], + ).toBeUndefined(); + }, + ); + }); + describe('commandPermissions', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'Should get a vaults permissions', + async () => { + const vaultId1 = await polykeyAgent.vaultManager.createVault(vaultName); + const vaultId2 = await polykeyAgent.vaultManager.createVault( + vaultName + '1', + ); + const vaultIdEncoded1 = vaultsUtils.encodeVaultId(vaultId1); + const vaultIdEncoded2 = vaultsUtils.encodeVaultId(vaultId2); + const targetNodeId = testUtils.generateRandomNodeId(); + const targetNodeIdEncoded = nodesUtils.encodeNodeId(targetNodeId); + await polykeyAgent.gestaltGraph.setNode({ + nodeId: targetNodeId, + }); + + // Creating permissions + await polykeyAgent.gestaltGraph.setGestaltAction( + ['node', targetNodeId], + 'scan', + ); + await polykeyAgent.acl.setVaultAction(vaultId1, targetNodeId, 'clone'); + await polykeyAgent.acl.setVaultAction(vaultId1, targetNodeId, 'pull'); + await polykeyAgent.acl.setVaultAction(vaultId2, targetNodeId, 'pull'); + + command = ['vaults', 'permissions', '-np', dataDir, vaultIdEncoded1]; + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(targetNodeIdEncoded); + expect(result.stdout).toContain('clone'); + expect(result.stdout).toContain('pull'); + + command = ['vaults', 'permissions', '-np', dataDir, vaultIdEncoded2]; + const result2 = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result2.exitCode).toBe(0); + expect(result2.stdout).toContain(targetNodeIdEncoded); + expect(result2.stdout).not.toContain('clone'); + expect(result2.stdout).toContain('pull'); + }, + ); + }); + describe('commandVaultVersion', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should switch the version of a vault', + async () => { + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const id = polykeyAgent.vaultManager.getVaultId(vaultName); + expect(id).toBeTruthy(); + + const secret1 = { name: 'Secret-1', content: 'Secret-1-content' }; + const secret2 = { name: 'Secret-1', content: 'Secret-2-content' }; + + const ver1Oid = await polykeyAgent.vaultManager.withVaults( + [vaultId], + async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secret1.name, secret1.content); + }); + const ver1Oid = (await vault.log(undefined, 1))[0].commitId; + + await vault.writeF(async (efs) => { + await efs.writeFile(secret2.name, secret2.content); + }); + return ver1Oid; + }, + ); + + const command = [ + 'vaults', + 'version', + '-np', + dataDir, + vaultName, + ver1Oid, + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const fileContents = await vault.readF(async (efs) => { + return (await efs.readFile(secret1.name)).toString(); + }); + expect(fileContents).toStrictEqual(secret1.content); + }); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should switch the version of a vault to the latest version', + async () => { + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const id = polykeyAgent.vaultManager.getVaultId(vaultName); + expect(id).toBeTruthy(); + + const secret1 = { name: 'Secret-1', content: 'Secret-1-content' }; + const secret2 = { name: 'Secret-1', content: 'Secret-2-content' }; + + const ver1Oid = await polykeyAgent.vaultManager.withVaults( + [vaultId], + async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secret1.name, secret1.content); + }); + const ver1Oid = (await vault.log(undefined, 1))[0].commitId; + + await vault.writeF(async (efs) => { + await efs.writeFile(secret2.name, secret2.content); + }); + return ver1Oid; + }, + ); + + const command = [ + 'vaults', + 'version', + '-np', + dataDir, + vaultName, + ver1Oid, + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + const command2 = [ + 'vaults', + 'version', + '-np', + dataDir, + vaultName, + 'last', + ]; + + const result2 = await testUtils.pkStdio([...command2], { + env: {}, + cwd: dataDir, + }); + expect(result2.exitCode).toBe(0); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should handle invalid version IDs', + async () => { + await polykeyAgent.vaultManager.createVault(vaultName); + const id = polykeyAgent.vaultManager.getVaultId(vaultName); + expect(id).toBeTruthy(); + + const command = [ + 'vaults', + 'version', + '-np', + dataDir, + vaultName, + 'NOT_A_VALID_CHECKOUT_ID', + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(sysexits.USAGE); + + expect(result.stderr).toContain('ErrorVaultReferenceInvalid'); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should throw an error if the vault is not found', + async () => { + const command = [ + 'vaults', + 'version', + '-np', + dataDir, + 'zLnM7puKobbh4YXEz66StAq', + 'NOT_A_VALID_CHECKOUT_ID', + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toBe(sysexits.USAGE); + expect(result.stderr).toContain('ErrorVaultsVaultUndefined'); + }, + ); + }); + describe('commandVaultLog', () => { + const secret1 = { name: 'secret1', content: 'Secret-1-content' }; + const secret2 = { name: 'secret2', content: 'Secret-2-content' }; + + let vaultId: VaultId; + let writeF1Oid: string; + let writeF2Oid: string; + let writeF3Oid: string; + + beforeEach(async () => { + vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secret1.name, secret1.content); + }); + writeF1Oid = (await vault.log(undefined, 0))[0].commitId; + + await vault.writeF(async (efs) => { + await efs.writeFile(secret2.name, secret2.content); + }); + writeF2Oid = (await vault.log(undefined, 0))[0].commitId; + + await vault.writeF(async (efs) => { + await efs.unlink(secret2.name); + }); + writeF3Oid = (await vault.log(undefined, 0))[0].commitId; + }); + }); + afterEach(async () => { + await polykeyAgent.vaultManager.destroyVault(vaultId); + }); + + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'Should get all writeFs', + async () => { + const command = ['vaults', 'log', '-np', dataDir, vaultName]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toEqual(0); + expect(result.stdout).toContain(writeF1Oid); + expect(result.stdout).toContain(writeF2Oid); + expect(result.stdout).toContain(writeF3Oid); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should get a part of the log', + async () => { + const command = ['vaults', 'log', '-np', dataDir, '-d', '2', vaultName]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toEqual(0); + expect(result.stdout).not.toContain(writeF1Oid); + expect(result.stdout).toContain(writeF2Oid); + expect(result.stdout).toContain(writeF3Oid); + }, + ); + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should get a specific writeF', + async () => { + const command = [ + 'vaults', + 'log', + '-np', + dataDir, + '-d', + '1', + vaultName, + '-ci', + writeF2Oid, + ]; + + const result = await testUtils.pkStdio([...command], { + env: {}, + cwd: dataDir, + }); + expect(result.exitCode).toEqual(0); + expect(result.stdout).not.toContain(writeF1Oid); + expect(result.stdout).toContain(writeF2Oid); + expect(result.stdout).not.toContain(writeF3Oid); + }, + ); + test.todo('test formatting of the output'); + }); + describe('commandScanNode', () => { + testUtils.testIf(testUtils.isTestPlatformEmpty)( + 'should return the vaults names and ids of the remote vault', + async () => { + let remoteOnline: PolykeyAgent | undefined; + try { + remoteOnline = await PolykeyAgent.createPolykeyAgent({ + password, + logger, + nodePath: path.join(dataDir, 'remoteOnline'), + networkConfig: { + agentHost: '127.0.0.1' as Host, + clientHost: '127.0.0.1' as Host, + }, + keyRingConfig: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }); + const remoteOnlineNodeId = remoteOnline.keyRing.getNodeId(); + const remoteOnlineNodeIdEncoded = + nodesUtils.encodeNodeId(remoteOnlineNodeId); + await polykeyAgent.nodeManager.setNode(remoteOnlineNodeId, { + host: remoteOnline.quicServerAgent.host as unknown as Host, + port: remoteOnline.quicServerAgent.port as unknown as Port, + } as NodeAddress); + + await remoteOnline.gestaltGraph.setNode({ + nodeId: polykeyAgent.keyRing.getNodeId(), + }); + + const commands1 = [ + 'vaults', + 'scan', + remoteOnlineNodeIdEncoded, + '-np', + dataDir, + ]; + const result1 = await testUtils.pkStdio(commands1, { + env: { PK_PASSWORD: 'password' }, + cwd: dataDir, + }); + expect(result1.exitCode).toEqual(sysexits.NOPERM); + expect(result1.stderr).toContain( + 'ErrorVaultsPermissionDenied: Permission was denied - Scanning is not allowed for', + ); + + await remoteOnline.gestaltGraph.setGestaltAction( + ['node', polykeyAgent.keyRing.getNodeId()], + 'notify', + ); + + const commands2 = [ + 'vaults', + 'scan', + remoteOnlineNodeIdEncoded, + '-np', + dataDir, + ]; + const result2 = await testUtils.pkStdio(commands2, { + env: { PK_PASSWORD: 'password' }, + cwd: dataDir, + }); + expect(result2.exitCode).toEqual(sysexits.NOPERM); + expect(result2.stderr).toContain( + 'ErrorVaultsPermissionDenied: Permission was denied - Scanning is not allowed for', + ); + + await remoteOnline.gestaltGraph.setGestaltAction( + ['node', polykeyAgent.keyRing.getNodeId()], + 'scan', + ); + + const vault1Id = await remoteOnline.vaultManager.createVault( + 'Vault1' as VaultName, + ); + const vault2Id = await remoteOnline.vaultManager.createVault( + 'Vault2' as VaultName, + ); + const vault3Id = await remoteOnline.vaultManager.createVault( + 'Vault3' as VaultName, + ); + const nodeId = polykeyAgent.keyRing.getNodeId(); + await remoteOnline.acl.setVaultAction(vault1Id, nodeId, 'clone'); + await remoteOnline.acl.setVaultAction(vault2Id, nodeId, 'pull'); + await remoteOnline.acl.setVaultAction(vault2Id, nodeId, 'clone'); + const commands3 = [ + 'vaults', + 'scan', + remoteOnlineNodeIdEncoded, + '-np', + dataDir, + ]; + const result3 = await testUtils.pkStdio(commands3, { + env: { PK_PASSWORD: 'password' }, + cwd: dataDir, + }); + expect(result3.exitCode).toBe(0); + expect(result3.stdout).toContain( + `Vault1\t\t${vaultsUtils.encodeVaultId(vault1Id)}\t\tclone`, + ); + expect(result3.stdout).toContain( + `Vault2\t\t${vaultsUtils.encodeVaultId(vault2Id)}\t\tpull,clone`, + ); + expect(result3.stdout).not.toContain( + `Vault3\t\t${vaultsUtils.encodeVaultId(vault3Id)}`, + ); + } finally { + await remoteOnline?.stop(); + } + }, + globalThis.defaultTimeout * 2, + ); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..724de442 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "noEmit": false, + "stripInternal": true + }, + "exclude": [ + "./tests/**/*", + "./scripts/**/*", + "./benches/**/*" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..a1204365 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "tsBuildInfoFile": "./dist/tsbuildinfo", + "incremental": true, + "sourceMap": true, + "declaration": true, + "allowJs": true, + "strictNullChecks": true, + "noImplicitAny": false, + "experimentalDecorators": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "module": "CommonJS", + "target": "ES2022", + "baseUrl": "./src", + "paths": { + "@": ["index"], + "@/*": ["*"] + }, + "noEmit": true + }, + "include": [ + "./src/**/*", + "./src/**/*.json", + "./tests/**/*", + "./scripts/**/*", + "./benches/**/*" + ], + "ts-node": { + "require": ["tsconfig-paths/register"], + "transpileOnly": true, + "swc": true + } +} diff --git a/utils.nix b/utils.nix new file mode 100644 index 00000000..b3e08f8b --- /dev/null +++ b/utils.nix @@ -0,0 +1,117 @@ +{ runCommandNoCC +, linkFarm +, nix-gitignore +, nodejs +, node2nix +, pkgs +, lib +, fetchurl +, fetchFromGitHub +}: + +rec { + # This removes the org scoping + basename = builtins.baseNameOf node2nixDev.packageName; + # Filter source to only what's necessary for building + src = nix-gitignore.gitignoreSource [ + # The `.git` itself should be ignored + ".git" + # Hidden files + "/.*" + # Nix files + "/*.nix" + # Benchmarks + "/benches" + # Docs + "/docs" + # Tests + "/tests" + "/jest.config.js" + ] ./.; + nodeVersion = builtins.elemAt (lib.versions.splitVersion nodejs.version) 0; + node2nixDrv = dev: runCommandNoCC "node2nix" {} '' + mkdir $out + ${node2nix}/bin/node2nix \ + ${lib.optionalString dev "--development"} \ + --input ${src}/package.json \ + --lock ${src}/package-lock.json \ + --node-env $out/node-env.nix \ + --output $out/node-packages.nix \ + --composition $out/default.nix \ + --nodejs-${nodeVersion} + ''; + node2nixProd = (import (node2nixDrv false) { inherit pkgs nodejs; }).nodeDependencies.override (attrs: { + # Use filtered source + src = src; + # Do not run build scripts during npm rebuild and npm install + npmFlags = "--ignore-scripts"; + # Do not run npm install, dependencies are installed by nix + dontNpmInstall = true; + }); + node2nixDev = (import (node2nixDrv true) { inherit pkgs nodejs; }).package.override (attrs: { + # Use filtered source + src = src; + # Do not run build scripts during npm rebuild and npm install + # They will be executed in the postInstall hook + npmFlags = "--ignore-scripts"; + # Show full compilation flags + NIX_DEBUG = 1; + # Don't set rpath for native addons + # Native addons do not require their own runtime search path + # because they dynamically loaded by the nodejs runtime + NIX_DONT_SET_RPATH = true; + NIX_NO_SELF_RPATH = true; + postInstall = '' + # Path to headers used by node-gyp for native addons + export npm_config_nodedir="${nodejs}" + # This will setup the typescript build + npm run build + ''; + }); + pkgBuilds = { + "3.4" = { + "linux-x64" = fetchurl { + url = "https://github.com/vercel/pkg-fetch/releases/download/v3.4/node-v18.5.0-linux-x64"; + sha256 = "0b7iimvh2gldvbqfjpx0qvzg8d59miv1ca03vwv6rb7c2bi5isi5"; + }; + "win32-x64" = fetchurl { + url = "https://github.com/vercel/pkg-fetch/releases/download/v3.4/node-v18.5.0-win-x64"; + sha256 = "0jxrxgcggpzzx54gaai24zfywhq6fr0nm75iihpn248hv13sdsg0"; + }; + "macos-x64" = fetchurl { + url = "https://github.com/vercel/pkg-fetch/releases/download/v3.4/node-v18.5.0-macos-x64"; + sha256 = "0dg46fw3ik2wxmhymcj3ih0wx5789f2fhfq39m6c1m52kvssgib3"; + }; + # No build for v18.15.0 macos-arm64 build + # "macos-arm64" = fetchurl { + # url = "https://github.com/vercel/pkg-fetch/releases/download/v3.4/node-v18.5.0-macos-arm64"; + # sha256 = "1znxssrwcg8nxfr03x1dfz49qq70ik33nj42dxr566vanayifa94"; + # }; + }; + }; + pkgCachePath = + let + pkgBuild = pkgBuilds."3.4"; + fetchedName = n: builtins.replaceStrings ["node"] ["fetched"] n; + in + linkFarm "pkg-cache" + [ + { + name = fetchedName pkgBuild.linux-x64.name; + path = pkgBuild.linux-x64; + } + { + name = fetchedName pkgBuild.win32-x64.name; + path = pkgBuild.win32-x64; + } + { + name = fetchedName pkgBuild.macos-x64.name; + path = pkgBuild.macos-x64; + } + # No build for v18.15 macos-arm64 build + # { + # name = fetchedName pkgBuild.macos-arm64.name; + # path = pkgBuild.macos-arm64; + # } + ]; +}