diff --git a/.github/workflows/publish-keria.yml b/.github/workflows/publish-keria.yml deleted file mode 100644 index 6def72be..00000000 --- a/.github/workflows/publish-keria.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Publish Docker image - -on: - workflow_dispatch: - inputs: - version: - required: true -jobs: - push_to_registry: - name: Push Docker image to Docker Hub - runs-on: ubuntu-latest - steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: weboftrust/keria - - # For multi-arch - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Cache Docker Layers - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: keri-${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - keri-${{ runner.os }}-buildx- - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: images/keria.dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - weboftrust/keria:${{ github.event.inputs.version }} - weboftrust/keria:latest - labels: ${{ github.event.inputs.version }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - - - name: Move Docker cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..037c5e24 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,132 @@ +name: Publish + +permissions: + contents: read + packages: write + +env: + DOCKER_REGISTRY: ${{ vars.DOCKER_REGISTRY || 'docker.io' }} + DOCKER_IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'weboftrust/keria' }} + NPM_PACKAGE_NAME: ${{ vars.NPM_PACKAGE_NAME || 'signify-ts' }} + RELEASE_TAG: ${{ inputs.latest == true && 'latest' || 'dev' }} + +on: + push: + branches: + - "main" + workflow_dispatch: + inputs: + latest: + description: "Publish :latest tag" + type: boolean + required: false + default: false + dryrun: + description: "Dry run, don't push" + type: boolean + required: false + default: false + +jobs: + publish_docker: + name: Publish docker image + runs-on: ubuntu-24.04 + steps: + - name: Checkout out the repo + uses: actions/checkout@v4 + + - name: Log in to container registry ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + # Uses github tokens for GHCR, otherwise configured repository secrets + # This way, forks can be configured to push their own images without + # having to modify the workflow. + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ env.DOCKER_REGISTRY == 'ghcr.io' && github.actor || secrets.DOCKER_USERNAME }} + password: ${{ env.DOCKER_REGISTRY == 'ghcr.io' && secrets.GITHUB_TOKEN || secrets.DOCKER_PASSWORD }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker buildx + uses: docker/setup-buildx-action@v3 + + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + version: "0.9.5" + + - name: Extract version + run: | + echo "VERSION=$(./version.sh)" >> $GITHUB_ENV + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + name=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=raw,value=${{ env.VERSION }} + type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && inputs.latest == true }} + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'workflow_dispatch' || inputs.dryrun != true }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Create summary + run: | + echo "## Docker Image Published 🐳" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" | while IFS= read -r tag; do + echo "docker pull $tag" >> $GITHUB_STEP_SUMMARY + done + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + publish_npm: + name: Publish NPM + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + registry-url: "https://registry.npmjs.org" + + - name: Extract version + run: | + echo "VERSION=$(./version.sh)" >> $GITHUB_ENV + + - name: Publish package + working-directory: ./packages/signify-ts + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_PUBLISH_TAG: ${{ inputs.latest == true && 'latest' || 'dev' }} + NPM_PACKAGE_VERSION: ${{ env.VERSION }} + NPM_PACKAGE_NAME: ${{ env.NPM_PACKAGE_NAME }} + DRY_RUN: ${{ inputs.dryrun || '' }} + run: ./publish.sh + + - name: Create summary + run: | + echo "## NPM Package Published 📦" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "npm install ${{ env.NPM_PACKAGE_NAME }}@${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/python-app-ci.yml b/.github/workflows/python-app-ci.yml index b511348e..ee953189 100644 --- a/.github/workflows/python-app-ci.yml +++ b/.github/workflows/python-app-ci.yml @@ -1,12 +1,8 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - name: GitHub Actions for KERIA on: push: branches: - "main" - - "development" pull_request: workflow_dispatch: @@ -16,24 +12,29 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-13, ubuntu-latest] + os: [macos-13, ubuntu-22.04, ubuntu-24.04, ubuntu-latest] steps: - uses: actions/checkout@v4 + - name: Set up Python 3.12.8 uses: actions/setup-python@v5 with: python-version: 3.12.8 + - name: Install uv uses: astral-sh/setup-uv@v3 with: version: "0.9.5" - name: Install dependencies run: make install-dev + - name: Lint changes run: make lint + - name: Check formatting run: make format-check + - name: Run core KERIA tests run: make test @@ -46,7 +47,7 @@ jobs: run: docker compose up --build --wait coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set up Python 3.12.8 diff --git a/.github/workflows/signify-ci.yml b/.github/workflows/signify-ci.yml new file mode 100644 index 00000000..cc7d1860 --- /dev/null +++ b/.github/workflows/signify-ci.yml @@ -0,0 +1,40 @@ +name: GitHub Actions for Signify TS + +on: + push: + branches: + - "main" + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Setup node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Install dependencies + run: npm ci + + - name: Lint changes + run: npm run lint + + - name: Check formatting + run: npm run format:check + + - name: Build + run: npm run build + + - name: Run tests + run: npm test + + - name: Build docker + run: docker compose up --build --wait + + - name: Run integration test + run: npm run test:integration diff --git a/.gitignore b/.gitignore index c075c6af..dce086b7 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ dmypy.json # IntelliJ files .idea/ + + +node_modules/ diff --git a/images/keria.dockerfile b/Dockerfile similarity index 100% rename from images/keria.dockerfile rename to Dockerfile diff --git a/docker-compose.yaml b/docker-compose.yaml index f2290934..459b192a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,10 +1,77 @@ +x-healthcheck: &healthcheck + interval: 2s + timeout: 3s + retries: 5 + start_period: 2s + +x-python-env: &python-env + PYTHONUNBUFFERED: 1 + PYTHONIOENCODING: UTF-8 + +configs: + wan.json: + content: | + { + "dt": "2022-01-20T12:57:59.823350+00:00", + "wan": { + "dt": "2022-01-20T12:57:59.823350+00:00", + "curls": ["http://127.0.0.1:5642/"] + } + } + wil.json: + content: | + { + "dt": "2022-01-20T12:57:59.823350+00:00", + "wil": { + "dt": "2022-01-20T12:57:59.823350+00:00", + "curls": ["http://127.0.0.1:5643/"] + } + } + wes.json: + content: | + { + "dt": "2022-01-20T12:57:59.823350+00:00", + "wes": { + "dt": "2022-01-20T12:57:59.823350+00:00", + "curls": ["http://127.0.0.1:5644/"] + } + } + services: keria: - build: - context: . - dockerfile: ./images/keria.dockerfile + build: ./ + environment: + <<: *python-env + volumes: + - ./scripts/keri/cf/keria.json:/keria/config/keri/cf/keria.json + command: start --config-dir /keria/config --config-file keria + network_mode: host healthcheck: test: curl http://localhost:3902/health - interval: 5s - timeout: 2s - retries: 10 + <<: *healthcheck + + vlei-server: + image: gleif/vlei:0.2.0 + environment: + <<: *python-env + command: vLEI-server -s ./schema/acdc -c ./samples/acdc/ -o ./samples/oobis/ + healthcheck: + test: curl -f http://localhost:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao + <<: *healthcheck + network_mode: host + + witness-demo: + image: weboftrust/keri-witness-demo:1.1.0 + environment: + <<: *python-env + healthcheck: + test: curl -f http://localhost:5642/oobi + <<: *healthcheck + configs: + - source: wan.json + target: /keripy/scripts/keri/cf/main/wan.json + - source: wes.json + target: /keripy/scripts/keri/cf/main/wes.json + - source: wil.json + target: /keripy/scripts/keri/cf/main/wil.json + network_mode: host diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..cecea7e8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4701 @@ +{ + "name": "keria", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "workspaces": [ + "packages/*" + ] + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.14.0.tgz", + "integrity": "sha512-c5X8fwPLOtUS8TVdqhynz9iV0GlOtFUT1ppXYzUUlEXe4kbZ/mvMT8wXoT8kCwUka+zsiloq7sD3pZ3+QVTuNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.14.0", + "@shikijs/langs": "^3.14.0", + "@shikijs/themes": "^3.14.0", + "@shikijs/types": "^3.14.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.4", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.4.tgz", + "integrity": "sha512-77MhyFgZ1zGMwtCpqsk532SJEc3IJmSOXKTCeWoMTAvPnQOkuOgxEip1n5pG5YX1IzCTJ4kCvPKr8xYyzWFdhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.5", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.5.tgz", + "integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "minimatch": "^5.0.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.15.0.tgz", + "integrity": "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.15.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.15.0.tgz", + "integrity": "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.15.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.15.0.tgz", + "integrity": "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.15.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.15.0.tgz", + "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.14.tgz", + "integrity": "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/libsodium-wrappers-sumo": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.8.tgz", + "integrity": "sha512-N2+df4MB/A+W0RAcTw7A5oxKgzD+Vh6Ye7lfjWIi5SdTzVLfHPzxUjhwPqHLO5Ev9fv/+VHl+sUaUuTg4fUPqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/libsodium-wrappers": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz", + "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.3", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "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-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "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, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/complex.js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", + "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "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, + "license": "ISC", + "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, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.4.tgz", + "integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BSD-3-Clause", + "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": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "license": "MIT" + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libsodium-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz", + "integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz", + "integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==", + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.7.15" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mathjs": { + "version": "12.4.3", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.3.tgz", + "integrity": "sha512-oHdGPDbp7gO873xxG90RLq36IuicuKvbpr/bBG5g9c8Obm/VsKVrK9uoRZZHUodohzlnmCEqfDzbR3LH6m+aAQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.24.4", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "4.3.4", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minami": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/minami/-/minami-1.2.3.tgz", + "integrity": "sha512-3f2QqqbUC1usVux0FkQMFYB73yd9JIxmHSn1dWQacizL6hOUaNu6mA3KxZ9SfiCc4qgcgq+5XP59+hP7URa1Dw==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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, + "license": "MIT" + }, + "node_modules/openapi-typescript": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.10.1.tgz", + "integrity": "sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.5", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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" + } + ], + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "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" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/signify-ts": { + "resolved": "packages/signify-ts", + "link": true + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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, + "license": "MIT" + }, + "node_modules/string-width-cjs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/structured-headers": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.5.0.tgz", + "integrity": "sha512-oLnmXSsjhud+LxRJpvokwP8ImEB2wTg8sg30buwfVViKMuluTv3BlOJHUX9VW9pJ2nQOxmx87Z0kB86O4cphag==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.5" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/typedoc": { + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.14.tgz", + "integrity": "sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.12.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.8.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", + "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.3", + "@typescript-eslint/parser": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.1.tgz", + "integrity": "sha512-qTl3VF7BvOupTR85Zc561sPEgxyUSNSvTQ9fit7DEMP7yPgvvIGm5Zfa1dOM+kOwWGNviK9uFM9ra77+OjK7lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "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, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/signify-ts": { + "version": "0.3.0-rc1", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.8.1", + "@noble/hashes": "^1.3.2", + "base64-js": "^1.5.1", + "libsodium-wrappers-sumo": "^0.7.9", + "mathjs": "^12.4.0", + "structured-headers": "^0.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/libsodium-wrappers-sumo": "^0.7.5", + "@types/node": "^22.13.11", + "@vitest/coverage-v8": "^3.0.9", + "bip39": "^3.1.0", + "eslint": "^9.22.0", + "eslint-config-prettier": "^10.1.1", + "globals": "^16.4.0", + "minami": "^1.2.3", + "openapi-typescript": "^7.9.1", + "prettier": "^3.5.3", + "ts-mockito": "^2.6.1", + "typedoc": "^0.28.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.27.0", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.9" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..afe4fa44 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "npm -w signify-ts run build", + "test": "npm -ws test", + "lint": "npm -ws run lint", + "format": "npm -ws run pretty", + "format:check": "npm -ws run pretty:check", + "test:integration": "npm -w signify-ts run test:integration" + } +} diff --git a/packages/signify-ts/.gitignore b/packages/signify-ts/.gitignore new file mode 100644 index 00000000..c659f56c --- /dev/null +++ b/packages/signify-ts/.gitignore @@ -0,0 +1,10 @@ +*.log +.DS_Store +node_modules +dist + + +# IntelliJ Project Files +.idea +coverage/* + diff --git a/packages/signify-ts/.prettierignore b/packages/signify-ts/.prettierignore new file mode 100644 index 00000000..61967f4e --- /dev/null +++ b/packages/signify-ts/.prettierignore @@ -0,0 +1,6 @@ +diagrams/ +docs/ +package.json +package-lock.json +.github/ +MAINTAINERS.md \ No newline at end of file diff --git a/packages/signify-ts/.prettierrc b/packages/signify-ts/.prettierrc new file mode 100644 index 00000000..ad9620ae --- /dev/null +++ b/packages/signify-ts/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 4 +} diff --git a/packages/signify-ts/LICENSE b/packages/signify-ts/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/packages/signify-ts/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/signify-ts/MAINTAINERS.md b/packages/signify-ts/MAINTAINERS.md new file mode 100644 index 00000000..a80c9771 --- /dev/null +++ b/packages/signify-ts/MAINTAINERS.md @@ -0,0 +1,34 @@ +# Maintainers + +This document lists the maintainers of this project. + +## Current Maintainers + +| Name | GitHub | Discord | email | +| --------------- | ----------------------------------------------------- | --------------- | -------------------------- | +| Fergal O'Connor | [iFergal](https://github.com/iFergal) | djlandlord | | +| Daniel Lenksjö | [lenkan](https://github.com/lenkan) | lenksjo_82594 | | +| Kent Bull | [kentbull](https://github.com/kentbull) | kentbull | | + +## Responsibilities + +Maintainers are responsible for: + +- Reviewing and approving pull requests +- Managing releases +- Maintaining code quality standards +- Responding to issues and community questions + +## Review Process + +All pull requests require approval from at least 2 out of 3 maintainers before merging. + +## Retired Maintainers + +| Name | GitHub | Discord | email | +| ----------------------- | ------------------------------------------------------- | --------------------- | ------------------------------------- | +| Phil Feairheller | [pfeairheller](https://github.com/pfeairheller) | philfeairheller_95942 | | +| Lance Byrd | [2byrds](https://github.com/2byrds) | megrimlance | | +| Rodolfo Miranda | [rodolfomiranda](https://github.com/rodolfomiranda) | rodo5833 | | +| Alex Andrei | [AlexAndrei98](https://github.com/AlexAndrei98) | | | +| Kevin Griffin | [m00sey](https://github.com/m00sey) | m00sey | | diff --git a/packages/signify-ts/README.md b/packages/signify-ts/README.md new file mode 100644 index 00000000..acb3e24c --- /dev/null +++ b/packages/signify-ts/README.md @@ -0,0 +1,191 @@ +# TypeScript implementation of Signify + +Project Name: signify-ts + +[![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.png?v=101)](https://github.com/ellerbrock/typescript-badges/) +[![Tests](https://github.com/WebOfTrust/signify-ts/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/WebOfTrust/signify-ts/actions/workflows/main.yml) +[![codecov](https://codecov.io/gh/WebOfTrust/signify-ts/branch/main/graph/badge.svg?token=K3GK7MCYVW)](https://codecov.io/gh/WebOfTrust/signify-ts) +[![Documentation](https://img.shields.io/badge/documentation-grey?)](https://weboftrust.github.io/signify-ts/) + +## Signify - KERI Signing at the Edge + +Of the five functions in a KERI agent, + +1. Key generation +2. Encrypted key storage +3. Event generation +4. Event signing +5. Event Validation + +Signify-TS splits off two, key generation and event signing into a TypeScript library to provide "signing at the edge". +It accomplishes this by using [libsodium](https://doc.libsodium.org/) to generate ed25519 key pairs for signing and x25519 key pairs for encrypting the +private keys, next public keys and salts used to generate the private keys. The encrypted private key and salts are then stored on a +remote cloud agent that never has access to the decryption keys. New key pair sets (current and next) will be generated +for inception and rotation events with only the public keys and blake3 hash of the next keys made available to the agent. + +The communication protocol between a Signify client and [KERI](https://github.com/WebOfTrust/keri) agent will encode all cryptographic primitives as CESR base64 +encoded strings for the initial implementation. Support for binary CESR can be added in the future. + +### Environment Setup + +The code is built using Typescript and running code locally requires a Mac or Linux OS. + +- Install [Node.js](https://nodejs.org) + +- Install dependencies: + ```bash + npm install + ``` + +Typescript source files needs to be transpiled before running scripts or integration tests + +- Generate types: + + To generate TypeScript types from KERIA OpenAPI docs dynamically + + ``` + npm run generate:types + ``` + + We can specify KERIA spec url by this command: + + ``` + SPEC_URL=http://localhost:3902/spec.yaml npm run generate:types + ``` + +- Build: + + ```bash + npm run build + ``` + +- ready() must be called before library is useable. Example minimum viable client code. + + ```javascript + import { randomPasscode, ready, SignifyClient, Tier } from 'signify-ts'; + + await ready(); + + const bran = randomPasscode(); + const url = 'http://127.0.0.1:3901'; + const boot_url = 'http://127.0.0.1:3903'; + const actualSignifyClient = new SignifyClient( + url, + bran, + Tier.low, + boot_url + ); + + console.log(actualSignifyClient); + ``` + +### Unit testing + +To run unit tests + +```bash +npm test +``` + +### Integration testing + +The integration tests depends on a local instance of KERIA, vLEI-Server and Witness Demo. These are specified in the [Docker Compose](./docker-compose.yaml) file. To start the dependencies, use docker compose: + +```bash +docker compose up --wait +``` + +If successful, it should print something like this: + +```bash +$ docker compose up --wait +[+] Running 4/4 + ✔ Network signify-ts_default Created 0.0s + ✔ Container signify-ts-vlei-server-1 Healthy 5.7s + ✔ Container signify-ts-keria-1 Healthy 6.2s + ✔ Container signify-ts-witness-demo-1 Healthy 6.2s +``` + +It is possible to change the keria image by using environment variables. For example, to use weboftrust/keria:0.1.3, do: + +```bash +export KERIA_IMAGE_TAG=0.1.3 +docker compose pull +docker compose up --wait +``` + +To use another repository, you can do: + +```bash +export KERIA_IMAGE=gleif/keria +docker compose pull +docker compose up --wait +``` + +Use the npm script "test:integration" to run all integration tests in sequence: + +```bash +npm run test:integration +``` + +To execute a specific integration test, you can use: + +```bash +npm run test:integration -- test-integration/credentials.test.ts +``` + +It is also possible to run the tests using local instances of vLEI, Keria, and witness network. Set the environment variable `TEST_ENVIRONMENT` to `local`, e.g: + +``` +TEST_ENVIRONMENT=local npm run test:integration test-integration/credentials.test.ts +``` + +This changes the discovery urls to use `localhost` instead of the hostnames inside the docker network. + +# Diagrams + +Account Creation Workflow + +![Account Creation](/diagrams/account-creation-workflow.png) + +![Account Creation Webpage](/diagrams/account-creation-webpage-workflow.png) + +# Publishing + +This package is published on npm: https://www.npmjs.com/package/signify-ts. + +If you need to publish a version under your own scope, you can use the [publish script](./publish.sh). This enables you to create development packages. For example: + +```bash +NPM_PACKAGE_SCOPE=@myorg DRY_RUN=1 ./publish.sh +npm notice Tarball Details +npm notice name: @myorg/signify-ts +npm notice version: 0.3.0-rc1-dev.8fa9919 +npm notice filename: myorg-signify-ts-0.3.0-rc1-dev.8fa9919.tgz +npm notice package size: 81.0 kB +npm notice unpacked size: 370.0 kB +npm notice shasum: 8c160bc99d9ec552e6c478c20922cae3388a8ace +npm notice integrity: sha512-WRuD5PKFN3WBl[...]xVieCIS0UpVeg== +npm notice total files: 96 +npm notice +npm notice Publishing to https://registry.npmjs.org/ with tag dev and default access (dry-run) ++ @myorg/signify-ts@0.3.0-rc1-dev.8fa9919 +``` + +Set the `NPM_PUBLISH_TAG` to `latest` to skip the commit hash suffix in the version: + +```bash +NPM_PUBLISH_TAG=latest NPM_PACKAGE_SCOPE=@myorg DRY_RUN=1 ./publish.sh +npm notice Tarball Details +npm notice name: @myorg/signify-ts +npm notice version: 0.3.0-rc1 +npm notice filename: myorg-signify-ts-0.3.0-rc1.tgz +npm notice package size: 80.9 kB +npm notice unpacked size: 370.0 kB +npm notice shasum: 8c9e4edcf19802e8acaf5996a36061a9c335b1c4 +npm notice integrity: sha512-V1y2W3zs4Ccsn[...]vKY3WWgalcuBQ== +npm notice total files: 96 +npm notice +npm notice Publishing to https://registry.npmjs.org/ with tag latest and default access (dry-run) ++ @myorg/signify-ts@0.3.0-rc1 +``` diff --git a/packages/signify-ts/codecov.yaml b/packages/signify-ts/codecov.yaml new file mode 100644 index 00000000..45ec0ff1 --- /dev/null +++ b/packages/signify-ts/codecov.yaml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + target: 55 + paths: ['src'] + patch: + default: + target: 75 + paths: ['src'] diff --git a/packages/signify-ts/diagrams/account-creation-webpage-workflow.mmd b/packages/signify-ts/diagrams/account-creation-webpage-workflow.mmd new file mode 100644 index 00000000..27230b85 --- /dev/null +++ b/packages/signify-ts/diagrams/account-creation-webpage-workflow.mmd @@ -0,0 +1,23 @@ +sequenceDiagram + actor u as User + participant a as Web Page App + participant s as Signify + participant c as Cloud Agent + u ->> a: Create Account + a ->>+ s: Generate Pub/Pri Keys + s ->>- a: Return new Pub/Pri Keypair + a ->>+ c: Request ICP Event Creation with Keys + note over s,c: This call can not be secured + note right of c: Creates ICP Event + c ->>- a: Return ICP event + a ->>+ s: Sign ICP Event + s ->>- a: Return Signed Event + a ->>+ c: Create Account with Signed ICP Event + note over s,c: Call Signed by new Keys + note right of c: Parses and Saves ICP + note right of c: Create Account with new AID + c ->>- a: Return New Account KeyState + a ->>+ s: Save Key Information + s ->>+ c: Save Key Information + c ->>- s: Key Information Saved + a ->> u: Return New Account Information \ No newline at end of file diff --git a/packages/signify-ts/diagrams/account-creation-webpage-workflow.png b/packages/signify-ts/diagrams/account-creation-webpage-workflow.png new file mode 100644 index 00000000..7adb4ade Binary files /dev/null and b/packages/signify-ts/diagrams/account-creation-webpage-workflow.png differ diff --git a/packages/signify-ts/diagrams/account-creation-workflow.mmd b/packages/signify-ts/diagrams/account-creation-workflow.mmd new file mode 100644 index 00000000..8102654f --- /dev/null +++ b/packages/signify-ts/diagrams/account-creation-workflow.mmd @@ -0,0 +1,18 @@ +sequenceDiagram + actor u as User + participant s as Signify + participant c as Cloud Agent + u ->> s: Create Account + s ->> s: Generate Pub/Pri Keys + s ->>+ c: Request ICP Event Creation with Keys + note over s,c: This call can not be secured + note right of c: Creates ICP Event + c ->>- s: Return ICP event + s ->>+ c: Create Account with Signed ICP Event + note over s,c: Call Signed by new Keys + note right of c: Parses and Saves ICP + note right of c: Create Account with new AID + c ->>- s: Return New Account KeyState + s ->>+ c: Save Key Information + c ->>- s: Key Information Saved + s ->> u: Return New Account Information \ No newline at end of file diff --git a/packages/signify-ts/diagrams/account-creation-workflow.png b/packages/signify-ts/diagrams/account-creation-workflow.png new file mode 100644 index 00000000..51b186ba Binary files /dev/null and b/packages/signify-ts/diagrams/account-creation-workflow.png differ diff --git a/packages/signify-ts/eslint.config.mjs b/packages/signify-ts/eslint.config.mjs new file mode 100644 index 00000000..27bc2da6 --- /dev/null +++ b/packages/signify-ts/eslint.config.mjs @@ -0,0 +1,39 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommended, + { rules: prettier.rules }, + { + // These are files with more lenient lint config because they have not been "fixed" yet + // Once a directory here is fixed, it should be removed from here so the strict rules applies + files: ['src/keri/app/**', 'src/keri/core/**', 'test-integration/**'], + languageOptions: { + globals: globals['shared-node-browser'], + }, + rules: { + 'prefer-const': 'warn', + 'no-var': 'warn', + 'no-self-assign': 'warn', + 'no-case-declarations': 'warn', + 'no-constant-condition': 'warn', + 'no-empty': 'warn', + '@typescript-eslint/no-non-null-asserted-optional-chain': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-namespace': 'warn', + // '@typescript-eslint/ban-types': 'warn', + '@typescript-eslint/no-unused-vars': 'warn', + }, + }, + { + files: ['scripts/*.js'], + languageOptions: { + globals: globals.node, + }, + } +); diff --git a/packages/signify-ts/package-lock.json b/packages/signify-ts/package-lock.json new file mode 100644 index 00000000..6381d67c --- /dev/null +++ b/packages/signify-ts/package-lock.json @@ -0,0 +1,4676 @@ +{ + "name": "signify-ts", + "version": "0.3.0-rc1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "signify-ts", + "version": "0.3.0-rc1", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.8.1", + "@noble/hashes": "^1.3.2", + "base64-js": "^1.5.1", + "libsodium-wrappers-sumo": "^0.7.9", + "mathjs": "^12.4.0", + "structured-headers": "^0.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/libsodium-wrappers-sumo": "^0.7.5", + "@types/node": "^22.13.11", + "@vitest/coverage-v8": "^3.0.9", + "bip39": "^3.1.0", + "eslint": "^9.22.0", + "eslint-config-prettier": "^10.1.1", + "globals": "^16.4.0", + "minami": "^1.2.3", + "openapi-typescript": "^7.9.1", + "prettier": "^3.5.3", + "ts-mockito": "^2.6.1", + "typedoc": "^0.28.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.27.0", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.9" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.12.2.tgz", + "integrity": "sha512-HKZPmO8OSSAAo20H2B3xgJdxZaLTwtlMwxg0967scnrDlPwe6j5+ULGHyIqwgTbFCn9yv/ff8CmfWZLE9YKBzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.12.2", + "@shikijs/langs": "^3.12.2", + "@shikijs/themes": "^3.12.2", + "@shikijs/types": "^3.12.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.3.tgz", + "integrity": "sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.5", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.5.tgz", + "integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "minimatch": "^5.0.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.51.0.tgz", + "integrity": "sha512-VyfldO8T/C5vAXBGIobrAnUE+VJNVLw5z9h4NgSDq/AJZWt/fXqdW+0PJbk+M74xz7yMDRiHtlsuDV7ew6K20w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.51.0.tgz", + "integrity": "sha512-Z3ujzDZgsEVSokgIhmOAReh9SGT2qloJJX2Xo1Q3nPU1EhCXrV0PbpR3r7DWRgozqnjrPZQkLe5cgBPIYp70Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.51.0.tgz", + "integrity": "sha512-T3gskHgArUdR6TCN69li5VELVAZK+iQ4iwMoSMNYixoj+56EC9lTj35rcxhXzIJt40YfBkvDy3GS+t5zh7zM6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.51.0.tgz", + "integrity": "sha512-Hh7n/fh0g5UjH6ATDF56Qdf5bzdLZKIbhp5KftjMYG546Ocjeyg15dxphCpH1FFY2PJ2G6MiOVL4jMq5VLTyrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.51.0.tgz", + "integrity": "sha512-0EddADb6FBvfqYoxwVom3hAbAvpSVUbZqmR1wmjk0MSZ06hn/UxxGHKRqEQDMkts7XiZjejVB+TLF28cDTU+gA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.51.0.tgz", + "integrity": "sha512-MpqaEDLo3JuVPF+wWV4mK7V8akL76WCz8ndfz1aVB7RhvXFO3k7yT7eu8OEuog4VTSyNu5ibvN9n6lgjq/qLEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.51.0.tgz", + "integrity": "sha512-WEWAGFNFFpvSWAIT3MYvxTkYHv/cJl9yWKpjhheg7ONfB0hetZt/uwBnM3GZqSHrk5bXCDYTFXg3jQyk/j7eXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.51.0.tgz", + "integrity": "sha512-9bxtxj8QoAp++LOq5PGDGkEEOpCDk9rOEHUcXadnijedDH8IXrBt6PnBa4Y6NblvGWdoxvXZYghZLaliTCmAng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.51.0.tgz", + "integrity": "sha512-DdqA+fARqIsfqDYkKo2nrWMp0kvu/wPJ2G8lZ4DjYhn+8QhrjVuzmsh7tTkhULwjvHTN59nWVzAixmOi6rqjNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.51.0.tgz", + "integrity": "sha512-2XVRNzcUJE1UJua8P4a1GXS5jafFWE+pQ6zhUbZzptOu/70p1F6+0FTi6aGPd6jNtnJqGMjtBCXancC2dhYlWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.51.0.tgz", + "integrity": "sha512-R8QhY0kLIPCAVXWi2yftDSpn7Jtejey/WhMoBESSfwGec5SKdFVupjxFlKoQ7clVRuaDpiQf7wNx3EBZf4Ey6g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.51.0.tgz", + "integrity": "sha512-I498RPfxx9cMv1KTHQ9tg2Ku1utuQm+T5B+Xro+WNu3FzAFSKp4awKfgMoZwjoPgNbaFGINaOM25cQW6WuBhiQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.51.0.tgz", + "integrity": "sha512-o8COudsb8lvtdm9ixg9aKjfX5aeoc2x9KGE7WjtrmQFquoCRZ9jtzGlonujE4WhvXFepTraWzT4RcwyDDeHXjA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.51.0.tgz", + "integrity": "sha512-0shJPgSXMdYzOQzpM5BJN2euXY1f8uV8mS6AnrbMcH2KrkNsbpMxWB1wp8UEdiJ1NtyBkCk3U/HfX5mEONBq6w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.51.0.tgz", + "integrity": "sha512-L7pV+ny7865jamSCQwyozBYjFRUKaTsPqDz7ClOtJCDu4paf2uAa0mrcHwSt4XxZP2ogFZS9uuitH3NXdeBEJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.51.0.tgz", + "integrity": "sha512-4YHhP+Rv3T3+H3TPbUvWOw5tuSwhrVhkHHZhk4hC9VXeAOKR26/IsUAT4FsB4mT+kfIdxxb1BezQDEg/voPO8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.51.0.tgz", + "integrity": "sha512-P7U7U03+E5w7WgJtvSseNLOX1UhknVPmEaqgUENFWfNxNBa1OhExT6qYGmyF8gepcxWSaSfJsAV5UwhWrYefdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.51.0.tgz", + "integrity": "sha512-FuD8g3u9W6RPwdO1R45hZFORwa1g9YXEMesAKP/sOi7mDqxjbni8S3zAXJiDcRfGfGBqpRYVuH54Gu3FTuSoEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.51.0.tgz", + "integrity": "sha512-zST+FdMCX3QAYfmZX3dp/Fy8qLUetfE17QN5ZmmFGPrhl86qvRr+E9u2bk7fzkIXsfQR30Z7ZRS7WMryPPn4rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.51.0.tgz", + "integrity": "sha512-U+qhoCVAZmTHCmUKxdQxw1jwAFNFXmOpMME7Npt5GTb1W/7itfgAgNluVOvyeuSeqW+dEQLFuNZF3YZPO8XkMg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.51.0.tgz", + "integrity": "sha512-z6UpFzMhXSD8NNUfCi2HO+pbpSzSWIIPgb1TZsEZjmZYtk6RUIC63JYjlFBwbBZS3jt3f1q6IGfkj3g+GnBt2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.12.2.tgz", + "integrity": "sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.12.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.12.2.tgz", + "integrity": "sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.12.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.12.2.tgz", + "integrity": "sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.12.2" + } + }, + "node_modules/@shikijs/types": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.12.2.tgz", + "integrity": "sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.14.tgz", + "integrity": "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/libsodium-wrappers-sumo": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.8.tgz", + "integrity": "sha512-N2+df4MB/A+W0RAcTw7A5oxKgzD+Vh6Ye7lfjWIi5SdTzVLfHPzxUjhwPqHLO5Ev9fv/+VHl+sUaUuTg4fUPqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/libsodium-wrappers": "*" + } + }, + "node_modules/@types/node": { + "version": "22.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", + "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", + "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/type-utils": "8.44.0", + "@typescript-eslint/utils": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.44.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", + "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", + "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.44.0", + "@typescript-eslint/types": "^8.44.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", + "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", + "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", + "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/utils": "8.44.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", + "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", + "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.44.0", + "@typescript-eslint/tsconfig-utils": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", + "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", + "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "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-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.5.tgz", + "integrity": "sha512-9SdXjNheSiE8bALAQCQQuT6fgQaoxJh7IRYrRGZ8/9nv8WhJeC1aXAwN8TbaOssGOukUvyvnkgD9+Yuykvl1aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.30", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "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, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/complex.js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", + "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.35.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "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, + "license": "ISC", + "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, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.4.tgz", + "integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", + "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BSD-3-Clause", + "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": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "license": "MIT" + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libsodium-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz", + "integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz", + "integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==", + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.7.15" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mathjs": { + "version": "12.4.3", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.3.tgz", + "integrity": "sha512-oHdGPDbp7gO873xxG90RLq36IuicuKvbpr/bBG5g9c8Obm/VsKVrK9uoRZZHUodohzlnmCEqfDzbR3LH6m+aAQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.24.4", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "4.3.4", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minami": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/minami/-/minami-1.2.3.tgz", + "integrity": "sha512-3f2QqqbUC1usVux0FkQMFYB73yd9JIxmHSn1dWQacizL6hOUaNu6mA3KxZ9SfiCc4qgcgq+5XP59+hP7URa1Dw==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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, + "license": "MIT" + }, + "node_modules/openapi-typescript": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.9.1.tgz", + "integrity": "sha512-9gJtoY04mk6iPMbToPjPxEAtfXZ0dTsMZtsgUI8YZta0btPPig9DJFP4jlerQD/7QOwYgb0tl+zLUpDf7vb7VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.5", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.1.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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" + } + ], + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.51.0.tgz", + "integrity": "sha512-7cR0XWrdp/UAj2HMY/Y4QQEUjidn3l2AY1wSeZoFjMbD8aOMPoV9wgTFYbrJpPzzvejDEini1h3CiUP8wLzxQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.51.0", + "@rollup/rollup-android-arm64": "4.51.0", + "@rollup/rollup-darwin-arm64": "4.51.0", + "@rollup/rollup-darwin-x64": "4.51.0", + "@rollup/rollup-freebsd-arm64": "4.51.0", + "@rollup/rollup-freebsd-x64": "4.51.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.51.0", + "@rollup/rollup-linux-arm-musleabihf": "4.51.0", + "@rollup/rollup-linux-arm64-gnu": "4.51.0", + "@rollup/rollup-linux-arm64-musl": "4.51.0", + "@rollup/rollup-linux-loong64-gnu": "4.51.0", + "@rollup/rollup-linux-ppc64-gnu": "4.51.0", + "@rollup/rollup-linux-riscv64-gnu": "4.51.0", + "@rollup/rollup-linux-riscv64-musl": "4.51.0", + "@rollup/rollup-linux-s390x-gnu": "4.51.0", + "@rollup/rollup-linux-x64-gnu": "4.51.0", + "@rollup/rollup-linux-x64-musl": "4.51.0", + "@rollup/rollup-openharmony-arm64": "4.51.0", + "@rollup/rollup-win32-arm64-msvc": "4.51.0", + "@rollup/rollup-win32-ia32-msvc": "4.51.0", + "@rollup/rollup-win32-x64-msvc": "4.51.0", + "fsevents": "~2.3.2" + } + }, + "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" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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, + "license": "MIT" + }, + "node_modules/string-width-cjs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/structured-headers": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.5.0.tgz", + "integrity": "sha512-oLnmXSsjhud+LxRJpvokwP8ImEB2wTg8sg30buwfVViKMuluTv3BlOJHUX9VW9pJ2nQOxmx87Z0kB86O4cphag==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.5" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/typedoc": { + "version": "0.28.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.13.tgz", + "integrity": "sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.12.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.8.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.0.tgz", + "integrity": "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.44.0", + "@typescript-eslint/parser": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/utils": "8.44.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "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, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/signify-ts/package.json b/packages/signify-ts/package.json new file mode 100644 index 00000000..fdcfd7dd --- /dev/null +++ b/packages/signify-ts/package.json @@ -0,0 +1,71 @@ +{ + "name": "signify-ts", + "version": "0.3.0-rc1", + "description": "Signing at the edge for KERI, ACDC, and KERIA", + "keywords": [ + "keri", + "acdc", + "keria", + "signify", + "signify-ts", + "decentralized identity", + "authentic data", + "zero trust architecture" + ], + "author": "Phil Feairheller", + "homepage": "https://github.com/WebOfTrust/signify-ts", + "repo": { + "type": "git", + "url": "git+https://github.com/WebOfTrust/signify-ts.git" + }, + "bugs": { + "url": "https://github.com/WebOfTrust/signify-ts/issues" + }, + "license": "Apache-2.0", + "exports": { + "import": "./dist/index.js" + }, + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "start": "npm run build -- --watch", + "generate:types": "node scripts/generate-types.js", + "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.json --noEmit", + "test": "vitest", + "prepare": "tsc -p tsconfig.build.json", + "test:integration": "vitest -c vitest.integration.ts", + "lint": "eslint src test test-integration scripts", + "generate:docs": "typedoc src/index.ts", + "pretty": "prettier --write .", + "pretty:check": "prettier --check ." + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/libsodium-wrappers-sumo": "^0.7.5", + "@types/node": "^22.13.11", + "@vitest/coverage-v8": "^3.0.9", + "bip39": "^3.1.0", + "eslint": "^9.22.0", + "eslint-config-prettier": "^10.1.1", + "globals": "^16.4.0", + "minami": "^1.2.3", + "openapi-typescript": "^7.9.1", + "prettier": "^3.5.3", + "ts-mockito": "^2.6.1", + "typedoc": "^0.28.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.27.0", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.9" + }, + "dependencies": { + "@noble/curves": "^1.8.1", + "@noble/hashes": "^1.3.2", + "base64-js": "^1.5.1", + "libsodium-wrappers-sumo": "^0.7.9", + "mathjs": "^12.4.0", + "structured-headers": "^0.5.0" + } +} diff --git a/packages/signify-ts/publish.sh b/packages/signify-ts/publish.sh new file mode 100755 index 00000000..56353b92 --- /dev/null +++ b/packages/signify-ts/publish.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +name=${NPM_PACKAGE_NAME:-"signify-ts"} +version=${NPM_PACKAGE_VERSION:?"NPM_PACKAGE_VERSION must be set"} +tag=${RELEASE_TAG:-"dev"} + +cp package.json package.json.bak +npm ci +npm run build +npm pkg set version="${version}" +npm pkg set name="${name}" + +if [ -z "$DRY_RUN" ]; then + npm publish --tag "${tag}" --access public +else + npm publish --tag "${tag}" --access public --dry-run +fi + +mv package.json.bak package.json diff --git a/packages/signify-ts/scripts/generate-types.js b/packages/signify-ts/scripts/generate-types.js new file mode 100644 index 00000000..67a646a6 --- /dev/null +++ b/packages/signify-ts/scripts/generate-types.js @@ -0,0 +1,33 @@ +import path from 'node:path'; +import { writeFile } from 'node:fs/promises'; +import openapiTS, { astToString } from 'openapi-typescript'; +import { isInterfaceDeclaration, isEnumDeclaration } from 'typescript'; + +const specUrl = process.env.SPEC_URL || 'http://localhost:3902/spec.yaml'; +const outputFile = path.resolve('src/types/keria-api-schema.ts'); + +console.log(`📦 Generating types from ${specUrl}`); +const ast = await openapiTS(new URL(specUrl), { + enum: true, + rootTypes: false, +}); + +// Filter to keep components interface, enums +const content = ast.filter((s) => { + // Keep enum declarations + if (isEnumDeclaration(s)) { + return true; + } + + // Keep components interface + if (isInterfaceDeclaration(s) && s.name.text === 'components') { + return true; + } + + return false; +}); + +const header = `// AUTO-GENERATED: Only components retained from OpenAPI schema\n\n`; +await writeFile(outputFile, `${header}${astToString(content)}`); + +console.log(`🚀 ${specUrl} → ${outputFile}`); diff --git a/packages/signify-ts/src/exports.ts b/packages/signify-ts/src/exports.ts new file mode 100644 index 00000000..7bcea75a --- /dev/null +++ b/packages/signify-ts/src/exports.ts @@ -0,0 +1,44 @@ +export * from './ready.ts'; + +export * from './keri/app/habery.ts'; +export * from './keri/app/controller.ts'; + +export * from './keri/app/aiding.ts'; +export * from './keri/app/clienting.ts'; +export * from './keri/app/contacting.ts'; +export * from './keri/app/coring.ts'; +export * from './keri/app/credentialing.ts'; +export * from './keri/app/escrowing.ts'; +export * from './keri/app/exchanging.ts'; +export * from './keri/app/grouping.ts'; +export * from './keri/app/notifying.ts'; + +export * from './keri/core/authing.ts'; +export * from './keri/core/cigar.ts'; +export * from './keri/core/cipher.ts'; +export * from './keri/core/core.ts'; +export * from './keri/core/counter.ts'; +export * from './keri/core/decrypter.ts'; +export * from './keri/core/diger.ts'; +export * from './keri/core/encrypter.ts'; +export * from './keri/core/eventing.ts'; +export * from './keri/core/httping.ts'; +export * from './keri/core/indexer.ts'; +export * from './keri/core/keeping.ts'; +export * from './keri/core/kering.ts'; +export * from './keri/core/manager.ts'; +export * from './keri/core/matter.ts'; +export * from './keri/core/number.ts'; +export * from './keri/core/prefixer.ts'; +export * from './keri/core/saider.ts'; +export * from './keri/core/salter.ts'; +export * from './keri/core/seqner.ts'; +export * from './keri/core/serder.ts'; +export * from './keri/core/siger.ts'; +export * from './keri/core/signer.ts'; +export * from './keri/core/tholder.ts'; +export * from './keri/core/utils.ts'; +export * from './keri/core/verfer.ts'; +export * from './keri/core/keyState.ts'; + +export * from './keri/end/ending.ts'; diff --git a/packages/signify-ts/src/index.ts b/packages/signify-ts/src/index.ts new file mode 100644 index 00000000..a1046886 --- /dev/null +++ b/packages/signify-ts/src/index.ts @@ -0,0 +1,3 @@ +import * as exp from './exports.ts'; +export * from './exports.ts'; +export default exp; diff --git a/packages/signify-ts/src/keri/app/aiding.ts b/packages/signify-ts/src/keri/app/aiding.ts new file mode 100644 index 00000000..51ba1303 --- /dev/null +++ b/packages/signify-ts/src/keri/app/aiding.ts @@ -0,0 +1,528 @@ +import { Tier } from '../core/salter.ts'; +import { Algos } from '../core/manager.ts'; +import { incept, interact, reply, rotate } from '../core/eventing.ts'; +import { b, Ilks, Serials, Vrsn_1_0 } from '../core/core.ts'; +import { Tholder } from '../core/tholder.ts'; +import { MtrDex } from '../core/matter.ts'; +import { Serder } from '../core/serder.ts'; +import { parseRangeHeaders } from '../core/httping.ts'; +import { IdentifierManagerFactory } from '../core/keeping.ts'; +import { HabState } from '../core/keyState.ts'; +import { components } from '../../types/keria-api-schema.ts'; + +/** Arguments required to create an identfier */ +export interface CreateIdentiferArgs { + transferable?: boolean; + isith?: string | number | string[]; + nsith?: string | number | string[]; + wits?: string[]; + toad?: number; + proxy?: string; + delpre?: string; + dcode?: string; + data?: any; + algo?: Algos; + pre?: string; + states?: any[]; + rstates?: any[]; + prxs?: any[]; + nxts?: any[]; + mhab?: HabState; + keys?: string[]; + ndigs?: string[]; + bran?: string; + count?: number; + ncount?: number; + tier?: Tier; + extern_type?: string; + extern?: any; +} + +/** Arguments required to rotate an identfier */ +export interface RotateIdentifierArgs { + transferable?: boolean; + nsith?: string | number | string[]; + toad?: number; + cuts?: string[]; + adds?: string[]; + data?: Array; + ncode?: string; + ncount?: number; + ncodes?: string[]; + states?: any[]; + rstates?: any[]; +} + +/** + * Reducing the SignifyClient dependencies used by Identifier class + */ +export interface IdentifierDeps { + fetch( + pathname: string, + method: string, + body: unknown, + headers?: Headers + ): Promise; + pidx: number; + manager: IdentifierManagerFactory | null; +} + +/** + * Updatable information for a managed identifier + */ +export interface IdentifierInfo { + name: string; +} + +export type GroupMembers = components['schemas']['GroupMember']; + +/** Identifier */ +export class Identifier { + public client: IdentifierDeps; + + /** + * Identifier + * @param {IdentifierDeps} client + */ + constructor(client: IdentifierDeps) { + this.client = client; + } + + /** + * List managed identifiers + * @async + * @param {number} [start=0] Start index of list of notifications, defaults to 0 + * @param {number} [end=24] End index of list of notifications, defaults to 24 + * @returns {Promise} A promise to the list of managed identifiers + */ + async list(start: number = 0, end: number = 24): Promise { + const extraHeaders = new Headers(); + extraHeaders.append('Range', `aids=${start}-${end}`); + + const path = `/identifiers`; + const data = null; + const method = 'GET'; + const res = await this.client.fetch(path, method, data, extraHeaders); + + const cr = res.headers.get('content-range'); + const range = parseRangeHeaders(cr, 'aids'); + const aids = await res.json(); + + return { + start: range.start, + end: range.end, + total: range.total, + aids: aids, + }; + } + + /** + * Get information for a managed identifier + * @async + * @param {string} name Prefix or alias of the identifier + * @returns {Promise} A promise to the identifier information + */ + async get(name: string): Promise { + const path = `/identifiers/${encodeURIComponent(name)}`; + const data = null; + const method = 'GET'; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } + + /** + * Update managed identifier + * @async + * @param {string} name Prefix or alias of the identifier + * @param {IdentifierInfo} info Information to update for the given identifier + * @returns {Promise} A promise to the identifier information after updating + */ + async update(name: string, info: IdentifierInfo): Promise { + const path = `/identifiers/${name}`; + const method = 'PUT'; + const res = await this.client.fetch(path, method, info); + return await res.json(); + } + + /** + * Create a managed identifier + * @async + * @param {string} name Name or alias of the identifier + * @param {CreateIdentiferArgs} [kargs] Optional parameters to create the identifier + * @returns {EventResult} The inception result + */ + async create( + name: string, + kargs: CreateIdentiferArgs = {} + ): Promise { + const algo = kargs.algo == undefined ? Algos.salty : kargs.algo; + + const transferable = kargs.transferable ?? true; + const isith = kargs.isith ?? '1'; + let nsith = kargs.nsith ?? '1'; + let wits = kargs.wits ?? []; + const toad = kargs.toad ?? 0; + let dcode = kargs.dcode ?? MtrDex.Blake3_256; + const proxy = kargs.proxy; + const delpre = kargs.delpre; + const data = kargs.data != undefined ? [kargs.data] : []; + const pre = kargs.pre; + const states = kargs.states; + const rstates = kargs.rstates; + const prxs = kargs.prxs; + const nxts = kargs.nxts; + const mhab = kargs.mhab; + const _keys = kargs.keys; + const _ndigs = kargs.ndigs; + const bran = kargs.bran; + const count = kargs.count; + let ncount = kargs.ncount; + const tier = kargs.tier; + const extern_type = kargs.extern_type; + const extern = kargs.extern; + + if (!transferable) { + ncount = 0; + nsith = 0; + dcode = MtrDex.Ed25519N; + } + + const xargs = { + transferable: transferable, + isith: isith, + nsith: nsith, + wits: wits, + toad: toad, + proxy: proxy, + delpre: delpre, + dcode: dcode, + data: data, + algo: algo, + pre: pre, + prxs: prxs, + nxts: nxts, + mhab: mhab, + states: states, + rstates: rstates, + keys: _keys, + ndigs: _ndigs, + bran: bran, + count: count, + ncount: ncount, + tier: tier, + extern_type: extern_type, + extern: extern, + }; + + const keeper = this.client.manager!.new(algo, this.client.pidx, xargs); + const [keys, ndigs] = await keeper!.incept(transferable); + wits = wits !== undefined ? wits : []; + let serder: Serder | undefined = undefined; + if (delpre == undefined) { + serder = incept({ + keys: keys!, + isith: isith, + ndigs: ndigs, + nsith: nsith, + toad: toad, + wits: wits, + cnfg: [], + data: data, + version: Vrsn_1_0, + kind: Serials.JSON, + code: dcode, + intive: false, + }); + } else { + serder = incept({ + keys: keys!, + isith: isith, + ndigs: ndigs, + nsith: nsith, + toad: toad, + wits: wits, + cnfg: [], + data: data, + version: Vrsn_1_0, + kind: Serials.JSON, + code: dcode, + intive: false, + delpre: delpre, + }); + } + + const sigs = await keeper!.sign(b(serder.raw)); + const jsondata: any = { + name: name, + icp: serder.sad, + sigs: sigs, + proxy: proxy, + smids: + states != undefined + ? states.map((state) => state.i) + : undefined, + rmids: + rstates != undefined + ? rstates.map((state) => state.i) + : undefined, + }; + jsondata[algo] = keeper.params(); + + this.client.pidx = this.client.pidx + 1; + const res = await this.client.fetch('/identifiers', 'POST', jsondata); + return new EventResult(serder, sigs, res); + } + + /** + * Generate an interaction event in a managed identifier + * @async + * @param {string} name Prefix or alias of the identifier + * @param {any} [data] Option data to be anchored in the interaction event + * @returns {Promise} A promise to the interaction event result + */ + async interact(name: string, data?: any): Promise { + const { serder, sigs, jsondata } = await this.createInteract( + name, + data + ); + + const res = await this.client.fetch( + '/identifiers/' + name + '/events', + 'POST', + jsondata + ); + return new EventResult(serder, sigs, res); + } + + async createInteract( + name: string, + data?: any + ): Promise<{ serder: any; sigs: any; jsondata: any }> { + const hab = await this.get(name); + const pre: string = hab.prefix; + + const state = hab.state; + const sn = parseInt(state.s, 16); + const dig = state.d; + + data = Array.isArray(data) ? data : [data]; + + const serder = interact({ + pre: pre, + sn: sn + 1, + data: data, + dig: dig, + version: undefined, + kind: undefined, + }); + const keeper = this.client!.manager!.get(hab); + const sigs = await keeper.sign(b(serder.raw)); + + const jsondata: any = { + ixn: serder.sad, + sigs: sigs, + }; + jsondata[keeper.algo] = keeper.params(); + return { serder, sigs, jsondata }; + } + + /** + * Generate a rotation event in a managed identifier + * @param {string} name Name or alias of the identifier + * @param {RotateIdentifierArgs} [kargs] Optional parameters requiered to generate the rotation event + * @returns {Promise} A promise to the rotation event result + */ + async rotate( + name: string, + kargs: RotateIdentifierArgs = {} + ): Promise { + const transferable = kargs.transferable ?? true; + const ncode = kargs.ncode ?? MtrDex.Ed25519_Seed; + const ncount = kargs.ncount ?? 1; + + const hab = await this.get(name); + const pre = hab.prefix; + const delegated = hab.state.di !== ''; + + const state = hab.state; + const count = state.k.length; + const dig = state.d; + const ridx = parseInt(state.s, 16) + 1; + const wits = state.b; + let isith = state.nt; + + let nsith = kargs.nsith ?? isith; + + // if isith is None: # compute default from newly rotated verfers above + if (isith == undefined) + isith = `${Math.max(1, Math.ceil(count / 2)).toString(16)}`; + + // if nsith is None: # compute default from newly rotated digers above + if (nsith == undefined) + nsith = `${Math.max(1, Math.ceil(ncount / 2)).toString(16)}`; + + const cst = new Tholder({ sith: isith }).sith; // current signing threshold + const nst = new Tholder({ sith: nsith }).sith; // next signing threshold + + // Regenerate next keys to sign rotation event + const keeper = this.client.manager!.get(hab); + // Create new keys for next digests + const ncodes = kargs.ncodes ?? new Array(ncount).fill(ncode); + + const states = kargs.states == undefined ? [] : kargs.states; + const rstates = kargs.rstates == undefined ? [] : kargs.rstates; + const [keys, ndigs] = await keeper!.rotate( + ncodes, + transferable, + states, + rstates + ); + + const cuts = kargs.cuts ?? []; + const adds = kargs.adds ?? []; + const data = kargs.data != undefined ? [kargs.data] : []; + const toad = kargs.toad; + const ilk = delegated ? Ilks.drt : Ilks.rot; + + const serder = rotate({ + pre: pre, + ilk: ilk, + keys: keys, + dig: dig, + sn: ridx, + isith: cst, + nsith: nst, + ndigs: ndigs, + toad: toad, + wits: wits, + cuts: cuts, + adds: adds, + data: data, + }); + + const sigs = await keeper.sign(b(serder.raw)); + + const jsondata: any = { + rot: serder.sad, + sigs: sigs, + smids: + states != undefined + ? states.map((state) => state.i) + : undefined, + rmids: + rstates != undefined + ? rstates.map((state) => state.i) + : undefined, + }; + jsondata[keeper.algo] = keeper.params(); + + const res = await this.client.fetch( + '/identifiers/' + name + '/events', + 'POST', + jsondata + ); + return new EventResult(serder, sigs, res); + } + + /** + * Authorize an endpoint provider in a given role for a managed identifier + * @remarks + * Typically used to authorize the agent to be the endpoint provider for the identifier in the role of `agent` + * @async + * @param {string} name Name or alias of the identifier + * @param {string} role Authorized role for eid + * @param {string} [eid] Optional qb64 of endpoint provider to be authorized + * @param {string} [stamp=now] Optional date-time-stamp RFC-3339 profile of iso8601 datetime. Now is the default if not provided + * @returns {Promise} A promise to the result of the authorization + */ + async addEndRole( + name: string, + role: string, + eid?: string, + stamp?: string + ): Promise { + const hab = await this.get(name); + const pre = hab.prefix; + + const rpy = this.makeEndRole(pre, role, eid, stamp); + const keeper = this.client.manager!.get(hab); + const sigs = await keeper.sign(b(rpy.raw)); + + const jsondata = { + rpy: rpy.sad, + sigs: sigs, + }; + + const res = await this.client.fetch( + '/identifiers/' + name + '/endroles', + 'POST', + jsondata + ); + return new EventResult(rpy, sigs, res); + } + + /** + * Generate an /end/role/add reply message + * @param {string} pre Prefix of the identifier + * @param {string} role Authorized role for eid + * @param {string} [eid] Optional qb64 of endpoint provider to be authorized + * @param {string} [stamp=now] Optional date-time-stamp RFC-3339 profile of iso8601 datetime. Now is the default if not provided + * @returns {Serder} The reply message + */ + private makeEndRole( + pre: string, + role: string, + eid?: string, + stamp?: string + ): Serder { + const data: any = { + cid: pre, + role: role, + }; + if (eid != undefined) { + data.eid = eid; + } + const route = '/end/role/add'; + return reply(route, data, stamp, undefined, Serials.JSON); + } + + /** + * Get the members of a group identifier + * @async + * @param {string} name - Name or alias of the identifier + * @returns {Promise} - A promise to the list of members + */ + async members(name: string): Promise { + const res = await this.client.fetch( + '/identifiers/' + name + '/members', + 'GET', + undefined + ); + return await res.json(); + } +} + +/** Event Result */ +export class EventResult { + private readonly _serder: Serder; + private readonly _sigs: string[]; + private readonly response: Response; + + constructor(serder: Serder, sigs: string[], response: Response) { + this._serder = serder; + this._sigs = sigs; + this.response = response; + } + + get serder() { + return this._serder; + } + + get sigs() { + return this._sigs; + } + + async op(): Promise { + return await this.response.json(); + } +} diff --git a/packages/signify-ts/src/keri/app/clienting.ts b/packages/signify-ts/src/keri/app/clienting.ts new file mode 100644 index 00000000..53debd7e --- /dev/null +++ b/packages/signify-ts/src/keri/app/clienting.ts @@ -0,0 +1,513 @@ +import { components } from '../../types/keria-api-schema.ts'; +import { Authenticater } from '../core/authing.ts'; +import { HEADER_SIG_TIME } from '../core/httping.ts'; +import { ExternalModule, IdentifierManagerFactory } from '../core/keeping.ts'; +import { Tier } from '../core/salter.ts'; + +import { Identifier } from './aiding.ts'; +import { Contacts, Challenges } from './contacting.ts'; +import { Agent, Controller } from './controller.ts'; +import { Oobis, Operations, KeyEvents, KeyStates, Config } from './coring.ts'; +import { Credentials, Ipex, Registries, Schemas } from './credentialing.ts'; +import { Delegations } from './delegating.ts'; +import { Escrows } from './escrowing.ts'; +import { Exchanges } from './exchanging.ts'; +import { Groups } from './grouping.ts'; +import { Notifications } from './notifying.ts'; + +const DEFAULT_BOOT_URL = 'http://localhost:3903'; + +// Export type outside the class +export type AgentResourceResult = components['schemas']['AgentResourceResult']; + +class State { + agent: any | null; + controller: any | null; + ridx: number; + pidx: number; + + constructor() { + this.agent = null; + this.controller = null; + this.pidx = 0; + this.ridx = 0; + } +} + +/** + * An in-memory key manager that can connect to a KERIA Agent and use it to + * receive messages and act as a proxy for multi-signature operations and delegation operations. + */ +export class SignifyClient { + public controller: Controller; + public url: string; + public bran: string; + public pidx: number; + public agent: Agent | null; + public authn: Authenticater | null; + public manager: IdentifierManagerFactory | null; + public tier: Tier; + public bootUrl: string; + public exteralModules: ExternalModule[]; + private _identifiers = new Identifier(this); + private _operations = new Operations(this); + private _keyEvents = new KeyEvents(this); + private _keyStates = new KeyStates(this); + private _oobis = new Oobis(this); + private _config = new Config(this); + private _delegations = new Delegations(this); + private _exchanges = new Exchanges(this); + private _groups = new Groups(this); + private _escrows = new Escrows(this); + private _credentials = new Credentials(this); + private _registries = new Registries(this); + private _ipex = new Ipex(this); + private _notifications = new Notifications(this); + private _contacts = new Contacts(this); + private _challenges = new Challenges(this); + private _schemas = new Schemas(this); + + /** + * SignifyClient constructor + * @param {string} url KERIA admin interface URL + * @param {string} bran Base64 21 char string that is used as base material for seed of the client AID + * @param {Tier} tier Security tier for generating keys of the client AID (high | mewdium | low) + * @param {string} bootUrl KERIA boot interface URL + * @param {ExternalModule[]} externalModules list of external modules to load + */ + constructor( + url: string, + bran: string, + tier: Tier = Tier.low, + bootUrl: string = DEFAULT_BOOT_URL, + externalModules: ExternalModule[] = [] + ) { + this.url = url; + if (bran.length < 21) { + throw Error('bran must be 21 characters'); + } + this.bran = bran; + this.pidx = 0; + this.controller = new Controller(bran, tier); + this.authn = null; + this.agent = null; + this.manager = null; + this.tier = tier; + this.bootUrl = bootUrl; + this.exteralModules = externalModules; + } + + get data() { + return [this.url, this.bran, this.pidx, this.authn]; + } + + /** + * Boot a KERIA agent + * @async + * @returns {Promise} A promise to the result of the boot + */ + async boot(): Promise { + const [evt, sign] = this.controller?.event ?? []; + const data = { + icp: evt.sad, + sig: sign.qb64, + stem: this.controller?.stem, + pidx: 1, + tier: this.controller?.tier, + }; + + return await fetch(this.bootUrl + '/boot', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + /** + * Get state of the agent and the client + * @async + * @returns {Promise} A promise to the state + */ + async state(): Promise { + const caid = this.controller?.pre; + + const res = await fetch(this.url + `/agent/${caid}`); + if (res.status == 404) { + throw new Error(`agent does not exist for controller ${caid}`); + } + + const data = (await res.json()) as AgentResourceResult; + const state = new State(); + state.agent = data.agent ?? {}; + state.controller = data.controller ?? {}; + state.ridx = data.ridx ?? 0; + state.pidx = data.pidx ?? 0; + return state; + } + + /** Connect to a KERIA agent + * @async + */ + async connect() { + const state = await this.state(); + this.pidx = state.pidx; + //Create controller representing the local client AID + this.controller = new Controller( + this.bran, + this.tier, + 0, + state.controller + ); + this.controller.ridx = state.ridx !== undefined ? state.ridx : 0; + // Create agent representing the AID of KERIA cloud agent + this.agent = new Agent(state.agent); + if (this.agent.anchor != this.controller.pre) { + throw Error( + 'commitment to controller AID missing in agent inception event' + ); + } + if (this.controller.serder.sad.s == 0) { + await this.approveDelegation(); + } + this.manager = new IdentifierManagerFactory( + this.controller.salter, + this.exteralModules + ); + this.authn = new Authenticater( + this.controller.signer, + this.agent.verfer! + ); + } + + /** + * Fetch a resource from the KERIA agent + * @async + * @param {string} path Path to the resource + * @param {string} method HTTP method + * @param {any} data Data to be sent in the body of the resource + * @param {Headers} [extraHeaders] Optional extra headers to be sent with the request + * @returns {Promise} A promise to the result of the fetch + */ + async fetch( + path: string, + method: string, + data: any, + extraHeaders?: Headers + ): Promise { + const headers = new Headers(); + let signed_headers = new Headers(); + const final_headers = new Headers(); + + headers.set('Signify-Resource', this.controller.pre); + headers.set( + HEADER_SIG_TIME, + new Date().toISOString().replace('Z', '000+00:00') + ); + headers.set('Content-Type', 'application/json'); + + const _body = method == 'GET' ? null : JSON.stringify(data); + + if (this.authn) { + signed_headers = this.authn.sign( + headers, + method, + path.split('?')[0] + ); + } else { + throw new Error('client need to call connect first'); + } + + signed_headers.forEach((value, key) => { + final_headers.set(key, value); + }); + if (extraHeaders !== undefined) { + extraHeaders.forEach((value, key) => { + final_headers.append(key, value); + }); + } + const res = await fetch(this.url + path, { + method: method, + body: _body, + headers: final_headers, + }); + if (!res.ok) { + const error = await res.text(); + const message = `HTTP ${method} ${path} - ${res.status} ${res.statusText} - ${error}`; + throw new Error(message); + } + const isSameAgent = + this.agent?.pre === res.headers.get('signify-resource'); + if (!isSameAgent) { + throw new Error('message from a different remote agent'); + } + + const verification = this.authn.verify( + res.headers, + method, + path.split('?')[0] + ); + if (verification) { + return res; + } else { + throw new Error('response verification failed'); + } + } + + /** + * Create a Signed Request to fetch a resource from an external URL with headers signed by an AID + * @async + * @param {string} aidName Name or alias of the AID to be used for signing + * @param {string} url URL of the requested resource + * @param {RequestInit} req Request options should include: + * - method: HTTP method + * - data Data to be sent in the body of the resource. + * If the data is a CESR JSON string then you should also set contentType to 'application/json+cesr' + * If the data is a FormData object then you should not set the contentType and the browser will set it to 'multipart/form-data' + * If the data is an object then you should use JSON.stringify to convert it to a string and set the contentType to 'application/json' + * - contentType Content type of the request. + * @returns {Promise} A promise to the created Request + */ + async createSignedRequest( + aidName: string, + url: string, + req: RequestInit + ): Promise { + const hab = await this.identifiers().get(aidName); + const keeper = this.manager!.get(hab); + + const authenticator = new Authenticater( + keeper.signers[0], + keeper.signers[0].verfer + ); + + const headers = new Headers(req.headers); + headers.set('Signify-Resource', hab['prefix']); + headers.set( + HEADER_SIG_TIME, + new Date().toISOString().replace('Z', '000+00:00') + ); + + const signed_headers = authenticator.sign( + new Headers(headers), + req.method ?? 'GET', + new URL(url).pathname + ); + req.headers = signed_headers; + + return new Request(url, req); + } + + /** + * Approve the delegation of the client AID to the KERIA agent + * @async + * @returns {Promise} A promise to the result of the approval + */ + async approveDelegation(): Promise { + const sigs = this.controller.approveDelegation(this.agent!); + + const data = { + ixn: this.controller.serder.sad, + sigs: sigs, + }; + + return await fetch( + this.url + '/agent/' + this.controller.pre + '?type=ixn', + { + method: 'PUT', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + /** + * Save old client passcode in KERIA agent + * @async + * @param {string} passcode Passcode to be saved + * @returns {Promise} A promise to the result of the save + */ + async saveOldPasscode(passcode: string): Promise { + const caid = this.controller?.pre; + const body = { salt: passcode }; + return await fetch(this.url + '/salt/' + caid, { + method: 'PUT', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + /** + * Delete a saved passcode from KERIA agent + * @async + * @returns {Promise} A promise to the result of the deletion + */ + async deletePasscode(): Promise { + const caid = this.controller?.pre; + return await fetch(this.url + '/salt/' + caid, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + /** + * Rotate the client AID + * @async + * @param {string} nbran Base64 21 char string that is used as base material for the new seed + * @param {Array} aids List of managed AIDs to be rotated + * @returns {Promise} A promise to the result of the rotation + */ + async rotate(nbran: string, aids: string[]): Promise { + const data = this.controller.rotate(nbran, aids); + return await fetch(this.url + '/agent/' + this.controller.pre, { + method: 'PUT', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + /** + * Get identifiers resource + * @returns {Identifier} + */ + identifiers(): Identifier { + return this._identifiers; + } + + /** + * Get OOBIs resource + * @returns {Oobis} + */ + oobis(): Oobis { + return this._oobis; + } + + /** + * Get operations resource + * @returns {Operations} + */ + operations(): Operations { + return this._operations; + } + + /** + * Get keyEvents resource + * @returns {KeyEvents} + */ + keyEvents(): KeyEvents { + return this._keyEvents; + } + + /** + * Get keyStates resource + * @returns {KeyStates} + */ + keyStates(): KeyStates { + return this._keyStates; + } + + /** + * Get credentials resource + * @returns {Credentials} + */ + credentials(): Credentials { + return this._credentials; + } + + /** + * Get IPEX resource + * @returns {Ipex} + */ + ipex(): Ipex { + return this._ipex; + } + + /** + * Get registries resource + * @returns {Registries} + */ + registries(): Registries { + return this._registries; + } + + /** + * Get schemas resource + * @returns {Schemas} + */ + schemas(): Schemas { + return this._schemas; + } + + /** + * Get challenges resource + * @returns {Challenges} + */ + challenges(): Challenges { + return this._challenges; + } + + /** + * Get contacts resource + * @returns {Contacts} + */ + contacts(): Contacts { + return this._contacts; + } + + /** + * Get notifications resource + * @returns {Notifications} + */ + notifications(): Notifications { + return this._notifications; + } + + /** + * Get escrows resource + * @returns {Escrows} + */ + escrows(): Escrows { + return this._escrows; + } + + /** + * Get groups resource + * @returns {Groups} + */ + groups(): Groups { + return this._groups; + } + + /** + * Get exchange resource + * @returns {Exchanges} + */ + exchanges(): Exchanges { + return this._exchanges; + } + + /** + * Get delegations resource + * @returns {Delegations} + */ + delegations(): Delegations { + return this._delegations; + } + + /** + * Get agent config resource + * @returns {Config} + */ + config(): Config { + return this._config; + } +} diff --git a/packages/signify-ts/src/keri/app/contacting.ts b/packages/signify-ts/src/keri/app/contacting.ts new file mode 100644 index 00000000..8ba731ca --- /dev/null +++ b/packages/signify-ts/src/keri/app/contacting.ts @@ -0,0 +1,197 @@ +import { SignifyClient } from './clienting.ts'; +import { Operation } from './coring.ts'; +import { components } from '../../types/keria-api-schema.ts'; + +export type Contact = components['schemas']['Contact']; + +export interface ContactInfo { + [key: string]: unknown; +} + +/** + * Contacts + */ +export class Contacts { + client: SignifyClient; + + /** + * Contacts + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * List contacts + * @async + * @param {string} [group] Optional group name to filter contacts + * @param {string} [filterField] Optional field name to filter contacts + * @param {string} [filterValue] Optional field value to filter contacts + * @returns {Promise} A promise to the list of contacts + */ + async list( + group?: string, + filterField?: string, + filterValue?: string + ): Promise { + const params = new URLSearchParams(); + if (group !== undefined) { + params.append('group', group); + } + if (filterField !== undefined && filterValue !== undefined) { + params.append('filter_field', filterField); + params.append('filter_value', filterValue); + } + + const path = `/contacts` + '?' + params.toString(); + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } + + /** + * Get a contact + * @async + * @param {string} pre Prefix of the contact + * @returns {Promise} A promise to the contact + */ + async get(pre: string): Promise { + const path = `/contacts/` + pre; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } + + /** + * Add a contact + * @async + * @param pre Prefix of the contact + * @param info Information about the contact + * @returns A promise to the result of the addition + */ + async add(pre: string, info: ContactInfo): Promise { + const path = `/contacts/` + pre; + const method = 'POST'; + + const res = await this.client.fetch(path, method, info); + return await res.json(); + } + + /** + * Delete a contact + * @async + * @param {string} pre Prefix of the contact + * @returns {Promise} + */ + async delete(pre: string): Promise { + const path = `/contacts/` + pre; + const method = 'DELETE'; + + await this.client.fetch(path, method, undefined); + } + + /** + * Update a contact + * @async + * @param {string} pre Prefix of the contact + * @param {any} info Updated information about the contact + * @returns {Promise} A promise to the result of the update + */ + async update(pre: string, info: ContactInfo): Promise { + const path = `/contacts/` + pre; + const method = 'PUT'; + + const res = await this.client.fetch(path, method, info); + return await res.json(); + } +} + +export type Challenge = components['schemas']['Challenge']; + +/** + * Challenges + */ +export class Challenges { + client: SignifyClient; + /** + * Challenges + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Generate a random challenge word list based on BIP39 + * @async + * @param {number} strength Integer representing the strength of the challenge. Typically 128 or 256 + * @returns {Promise} A promise to the list of random words + */ + async generate(strength: number = 128): Promise { + const path = `/challenges?strength=${strength.toString()}`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } + + /** + * Respond to a challenge by signing a message with the list of words + * @async + * @param name Name or alias of the identifier + * @param recipient Prefix of the recipient of the response + * @param words List of words to embed in the signed response + * @returns A promise to the result of the response + */ + async respond( + name: string, + recipient: string, + words: string[] + ): Promise { + const hab = await this.client.identifiers().get(name); + const exchanges = this.client.exchanges(); + const resp = await exchanges.send( + name, + 'challenge', + hab, + '/challenge/response', + { words: words }, + {}, + [recipient] + ); + return resp; + } + + /** + * Ask Agent to verify a given sender signed the provided words + * @param source Prefix of the identifier that was challenged + * @param words List of challenge words to check for + * @returns A promise to the long running operation + */ + async verify(source: string, words: string[]): Promise> { + const path = `/challenges_verify/${source}`; + const method = 'POST'; + const data = { + words: words, + }; + const res = await this.client.fetch(path, method, data); + + return await res.json(); + } + + /** + * Mark challenge response as signed and accepted + * @param source Prefix of the identifier that was challenged + * @param said qb64 AID of exn message representing the signed response + * @returns {Promise} A promise to the result + */ + async responded(source: string, said: string): Promise { + const path = `/challenges_verify/${source}`; + const method = 'PUT'; + const data = { + said: said, + }; + const res = await this.client.fetch(path, method, data); + return res; + } +} diff --git a/packages/signify-ts/src/keri/app/controller.ts b/packages/signify-ts/src/keri/app/controller.ts new file mode 100644 index 00000000..2495ee9e --- /dev/null +++ b/packages/signify-ts/src/keri/app/controller.ts @@ -0,0 +1,433 @@ +import { SaltyCreator } from '../core/manager.ts'; +import { Salter, Tier } from '../core/salter.ts'; +import { MtrDex } from '../core/matter.ts'; +import { Diger } from '../core/diger.ts'; +import { incept, rotate, interact } from '../core/eventing.ts'; +import { Serder } from '../core/serder.ts'; +import { Tholder } from '../core/tholder.ts'; +import { Ilks, b, Serials, Vrsn_1_0 } from '../core/core.ts'; +import { Verfer } from '../core/verfer.ts'; +import { Encrypter } from '../core/encrypter.ts'; +import { Decrypter } from '../core/decrypter.ts'; +import { Cipher } from '../core/cipher.ts'; +import { Seqner } from '../core/seqner.ts'; +import { CesrNumber } from '../core/number.ts'; + +/** + * Agent is a custodial entity that can be used in conjuntion with a local Client to establish the + * KERI "signing at the edge" semantic + */ +export class Agent { + pre: string; + anchor: string; + verfer: Verfer | null; + state: any | null; + sn: number | undefined; + said: string | undefined; + + constructor(agent: any) { + this.pre = ''; + this.anchor = ''; + this.verfer = null; + this.state = null; + this.sn = 0; + this.said = ''; + this.parse(agent); + } + + private parse(agent: Agent) { + const [state, verfer] = this.event(agent); + + this.sn = new CesrNumber({}, undefined, state['s']).num; + this.said = state['d']; + + if (state['et'] !== Ilks.dip) { + throw new Error(`invalid inception event type ${state['et']}`); + } + + this.pre = state['i']; + if (!state['di']) { + throw new Error('no anchor to controller AID'); + } + + this.anchor = state['di']; + + this.verfer = verfer; + this.state = state; + } + + private event(evt: any): [any, Verfer, Diger] { + if (evt['k'].length !== 1) { + throw new Error(`agent inception event can only have one key`); + } + + const verfer = new Verfer({ qb64: evt['k'][0] }); + + if (evt['n'].length !== 1) { + throw new Error(`agent inception event can only have one next key`); + } + + const diger = new Diger({ qb64: evt['n'][0] }); + + const tholder = new Tholder({ sith: evt['kt'] }); + if (tholder.num !== 1) { + throw new Error(`invalid threshold ${tholder.num}, must be 1`); + } + + const ntholder = new Tholder({ sith: evt['nt'] }); + if (ntholder.num !== 1) { + throw new Error( + `invalid next threshold ${ntholder.num}, must be 1` + ); + } + return [evt, verfer, diger]; + } +} + +/** + * Controller is responsible for managing signing keys for the client and agent. The client + * signing key represents the Account for the client on the agent + */ +export class Controller { + /* + The bran is the combination of the first 21 characters of the passcode passed in prefixed with 'A' and '0A'. + Looks like: '0A' + 'A' + 'thisismysecretkeyseed' + Or: "0AAthisismysecretkeyseed" + + This is interpreted as encoded Base64URLSafe characters when used as the salt for key generation. + */ + private bran: string; + /** + * The stem is the prefix for the stretched input bytes the controller's cryptographic + * key pairs are derived from. + */ + public stem: string; + /** + * The security tier for the identifiers created by this Controller. + */ + public tier: Tier; + /** + * The rotation index used during key generation by this Controller. + */ + public ridx: number; + /** + * The salter is a cryptographic salt used to derive the controller's cryptographic key pairs + * and is deterministically derived from the bran and the security tier. + */ + public salter: any; + /** + * The current signing key used to sign requests for this controller. + */ + public signer: any; + /** + * The next signing key of which a digest is committed to in an establishment event (inception or rotation) to become the + * signing key after the next rotation. + * @private + */ + private nsigner: any; + /** + * Either the current establishment event, inception or rotation, or the interaction event used for delegation approval. + */ + public serder: Serder; + /** + * Current public keys formatted in fully-qualified Base64. + * @private + */ + private keys: string[]; + /** + * Digests of the next public keys formatted in fully-qualified Base64. + */ + public ndigs: string[]; + + /** + * Creates a Signify Controller starting at key index 0 that generates keys in + * memory based on the provided seed, or bran, the tier, and the rotation index. + * + * The rotation index is used as follows: + * + * @param bran + * @param tier + * @param ridx + * @param state + */ + constructor( + bran: string, + tier: Tier, + ridx: number = 0, + state: any | null = null + ) { + this.bran = MtrDex.Salt_128 + 'A' + bran.substring(0, 21); // qb64 salt for seed + this.stem = 'signify:controller'; + this.tier = tier; + this.ridx = ridx; + const codes = undefined; // Defines the types of seeds that the SaltyCreator will create. Defaults to undefined. + const keyCount = 1; // The number of keys to create. Defaults to 1. + const transferable = true; // Whether the keys are transferable. Defaults to true. + const code = MtrDex.Ed25519_Seed; // The type cryptographic seed to create by default when not overiddeen by "codes". + const pidx = 0; // The index of this identifier prefix of all managed identifiers created for this SignifyClient Controller. Defaults to 0. + const kidx = 0; // The overall starting key index for the first key this rotation set of keys. This is not a local index to this set of keys but an index in the overall set of keys for all keys in this sequence. + // Defaults to 0. Multiply rotation index (ridx) times key count to get the overall key index. + + this.salter = new Salter({ qb64: this.bran, tier: this.tier }); + + const creator = new SaltyCreator( + this.salter.qb64, + this.tier, + this.stem + ); + + // Creates the first key pair used to sign the inception event. + // noinspection UnnecessaryLocalVariableJS + const initialKeyIndex = ridx; // will be zero for inception + this.signer = creator + .create( + codes, + keyCount, + code, + transferable, + pidx, + initialKeyIndex, + kidx + ) + .signers.pop(); // assumes only one key pair is created because keyCount is 1 + + // Creates the second key pair which a digest of the public key is committed to in the inception event. + const nextKeyIndex = ridx + 1; + this.nsigner = creator + .create( + codes, + keyCount, + code, + transferable, + pidx, + nextKeyIndex, + kidx + ) + .signers.pop(); // assumes only one key pair is created because keyCount is 1 + this.keys = [this.signer.verfer.qb64]; + this.ndigs = [ + new Diger({ code: MtrDex.Blake3_256 }, this.nsigner.verfer.qb64b) + .qb64, + ]; + + if (state == null || state['ee']['s'] == 0) { + this.serder = incept({ + keys: this.keys, + isith: '1', + nsith: '1', + ndigs: this.ndigs, + code: MtrDex.Blake3_256, + toad: '0', + wits: [], + }); + } else { + this.serder = new Serder(state['ee']); + } + } + + approveDelegation(_agent: Agent) { + const seqner = new Seqner({ sn: _agent.sn }); + const anchor = { i: _agent.pre, s: seqner.snh, d: _agent.said }; + const sn = new CesrNumber({}, undefined, this.serder.sad['s']).num + 1; + this.serder = interact({ + pre: this.serder.pre, + dig: this.serder.sad['d'], + sn: sn, + data: [anchor], + version: Vrsn_1_0, + kind: Serials.JSON, + }); + return [this.signer.sign(this.serder.raw, 0).qb64]; + } + + get pre(): string { + return this.serder.pre; + } + + get event() { + const siger = this.signer.sign(this.serder.raw, 0); + return [this.serder, siger]; + } + + get verfers(): [] { + return this.signer.verfer(); + } + + derive(state: any) { + if (state != undefined && state['ee']['s'] === '0') { + return incept({ + keys: this.keys, + isith: '1', + nsith: '1', + ndigs: this.ndigs, + code: MtrDex.Blake3_256, + toad: '0', + wits: [], + }); + } else { + return new Serder({ sad: state.controller['ee'] }); + } + } + + rotate(bran: string, aids: Array) { + const nbran = MtrDex.Salt_128 + 'A' + bran.substring(0, 21); // qb64 salt for seed + const nsalter = new Salter({ qb64: nbran, tier: this.tier }); + const nsigner = this.salter.signer(undefined, false); + + const creator = new SaltyCreator( + this.salter.qb64, + this.tier, + this.stem + ); + const signer = creator + .create( + undefined, + 1, + MtrDex.Ed25519_Seed, + true, + 0, + this.ridx + 1, + 0, + false + ) + .signers.pop(); + + const ncreator = new SaltyCreator(nsalter.qb64, this.tier, this.stem); + this.signer = ncreator + .create( + undefined, + 1, + MtrDex.Ed25519_Seed, + true, + 0, + this.ridx, + 0, + false + ) + .signers.pop(); + this.nsigner = ncreator + .create( + undefined, + 1, + MtrDex.Ed25519_Seed, + true, + 0, + this.ridx + 1, + 0, + false + ) + .signers.pop(); + + this.keys = [this.signer.verfer.qb64, signer?.verfer.qb64]; + this.ndigs = [new Diger({}, this.nsigner.verfer.qb64b).qb64]; + + const rot = rotate({ + pre: this.pre, + keys: this.keys, + dig: this.serder.sad['d'], + isith: ['1', '0'], + nsith: '1', + ndigs: this.ndigs, + }); + + const sigs = [ + signer?.sign(b(rot.raw), 1, false, 0).qb64, + this.signer.sign(rot.raw, 0).qb64, + ]; + const encrypter = new Encrypter({}, b(nsigner.verfer.qb64)); + const decrypter = new Decrypter({}, nsigner.qb64b); + const sxlt = encrypter.encrypt(b(this.bran)).qb64; + + const keys: Record = {}; + + for (const aid of aids) { + const pre: string = aid['prefix'] as string; + if ('salty' in aid) { + const salty: any = aid['salty']; + const cipher = new Cipher({ qb64: salty['sxlt'] }); + const dnxt = decrypter.decrypt(null, cipher).qb64; + + // Now we have the AID salt, use it to verify against the current public keys + const acreator = new SaltyCreator( + dnxt, + salty['tier'], + salty['stem'] + ); + const signers = acreator.create( + salty['icodes'], + undefined, + MtrDex.Ed25519_Seed, + salty['transferable'], + salty['pidx'], + 0, + salty['kidx'], + false + ); + const _signers = []; + for (const signer of signers.signers) { + _signers.push(signer.verfer.qb64); + } + const pubs = aid['state']['k']; + + if (pubs.join(',') != _signers.join(',')) { + throw new Error('Invalid Salty AID'); + } + + const asxlt = encrypter.encrypt(b(dnxt)).qb64; + keys[pre] = { + sxlt: asxlt, + }; + } else if ('randy' in aid) { + const randy = aid['randy']; + const prxs = randy['prxs']; + const nxts = randy['nxts']; + + const nprxs = []; + const signers = []; + for (const prx of prxs) { + const cipher = new Cipher({ qb64: prx }); + const dsigner = decrypter.decrypt(null, cipher, true); + signers.push(dsigner); + nprxs.push(encrypter.encrypt(b(dsigner.qb64)).qb64); + } + const pubs = aid['state']['k']; + const _signers = []; + for (const signer of signers) { + _signers.push(signer.verfer.qb64); + } + + if (pubs.join(',') != _signers.join(',')) { + throw new Error( + `unable to rotate, validation of encrypted public keys ${pubs} failed` + ); + } + + const nnxts = []; + for (const nxt of nxts) { + nnxts.push(this.recrypt(nxt, decrypter, encrypter)); + } + + keys[pre] = { + prxs: nprxs, + nxts: nnxts, + }; + } else { + throw new Error('invalid aid type '); + } + } + + const data = { + rot: rot.sad, + sigs: sigs, + sxlt: sxlt, + keys: keys, + }; + return data; + } + + recrypt(enc: string, decrypter: Decrypter, encrypter: Encrypter) { + const cipher = new Cipher({ qb64: enc }); + const dnxt = decrypter.decrypt(null, cipher).qb64; + return encrypter.encrypt(b(dnxt)).qb64; + } +} diff --git a/packages/signify-ts/src/keri/app/coring.ts b/packages/signify-ts/src/keri/app/coring.ts new file mode 100644 index 00000000..0969de89 --- /dev/null +++ b/packages/signify-ts/src/keri/app/coring.ts @@ -0,0 +1,306 @@ +import { SignifyClient } from './clienting.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Salter } from '../core/salter.ts'; +import { Matter, MtrDex } from '../core/matter.ts'; +import { components } from '../../types/keria-api-schema.ts'; + +type OOBI = components['schemas']['OOBI']; + +export function randomPasscode(): string { + const raw = libsodium.randombytes_buf(16); + const salter = new Salter({ raw: raw }); + + // https://github.com/WebOfTrust/signify-ts/issues/242 + return salter.qb64.substring(2, 23); +} + +export function randomNonce(): string { + const seed = libsodium.randombytes_buf(libsodium.crypto_sign_SEEDBYTES); + const seedqb64 = new Matter({ raw: seed, code: MtrDex.Ed25519_Seed }); + return seedqb64.qb64; +} + +export class Oobis { + public client: SignifyClient; + /** + * Oobis + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Get the OOBI(s) for a managed indentifier for a given role + * @param {string} name Name or alias of the identifier + * @param {string} role Authorized role + * @returns {Promise} A promise to the OOBI(s) + */ + async get(name: string, role: string = 'agent'): Promise { + const path = `/identifiers/${name}/oobis?role=${role}`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } + + /** + * Resolve an OOBI + * @async + * @param {string} oobi The OOBI to be resolver + * @param {string} [alias] Optional name or alias to link the OOBI resolution to a contact + * @returns {Promise} A promise to the long-running operation + */ + async resolve(oobi: string, alias?: string): Promise { + const path = `/oobis`; + const data: any = { + url: oobi, + }; + if (alias !== undefined) { + data.oobialias = alias; + } + const method = 'POST'; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } +} + +// TODO: the generic will be replaced by specific overrides like IpexOperation +export type Operation = Omit< + components['schemas']['Operation'], + 'response' | 'metadata' +> & { + response?: T; + metadata?: { + depends?: Operation; + [property: string]: any; + }; +}; + +export interface OperationsDeps { + fetch( + pathname: string, + method: string, + body: unknown, + headers?: Headers + ): Promise; +} + +export interface AgentConfig { + iurls?: string[]; +} + +/** + * Operations + * @remarks + * Operations represent the status and result of long running tasks performed by KERIA agent + */ +export class Operations { + public client: OperationsDeps; + /** + * Operations + * @param {SignifyClient} client + */ + constructor(client: OperationsDeps) { + this.client = client; + } + + /** + * Get operation status + * @async + * @param {string} name Name of the operation + * @returns {Promise} A promise to the status of the operation + */ + async get(name: string): Promise> { + const path = `/operations/${name}`; + const data = null; + const method = 'GET'; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } + /** + * List operations + * @async + * @param {string} type Select operations by type + * @returns {Promise} A list of operations + */ + async list(type?: string): Promise[]> { + const params = new URLSearchParams(); + if (type !== undefined) { + params.append('type', type); + } + const path = `/operations?${params.toString()}`; + const data = null; + const method = 'GET'; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } + /** + * Delete operation + * @async + * @param {string} name Name of the operation + */ + async delete(name: string): Promise { + const path = `/operations/${name}`; + const data = null; + const method = 'DELETE'; + await this.client.fetch(path, method, data); + } + + /** + * Poll for operation to become completed. + */ + async wait( + op: Operation, + options: { + signal?: AbortSignal; + minSleep?: number; + maxSleep?: number; + increaseFactor?: number; + } = {} + ): Promise> { + const minSleep = options.minSleep ?? 10; + const maxSleep = options.maxSleep ?? 10000; + const increaseFactor = options.increaseFactor ?? 50; + + if (op.metadata?.depends?.done === false) { + await this.wait(op.metadata.depends, options); + } + + if (op.done === true) { + return op; + } + + let retries = 0; + + while (true) { + op = await this.get(op.name); + + const delay = Math.max( + minSleep, + Math.min(maxSleep, 2 ** retries * increaseFactor) + ); + retries++; + + if (op.done === true) { + return op; + } + + await new Promise((resolve) => setTimeout(resolve, delay)); + options.signal?.throwIfAborted(); + } + } +} + +/** + * KeyEvents + */ +export class KeyEvents { + public client: SignifyClient; + /** + * KeyEvents + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Retrieve key events for an identifier + * @async + * @param {string} pre Identifier prefix + * @returns {Promise} A promise to the key events + */ + async get(pre: string): Promise { + const path = `/events?pre=${pre}`; + const data = null; + const method = 'GET'; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } +} + +/** + * KeyStates + */ +export class KeyStates { + public client: SignifyClient; + /** + * KeyStates + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Retriene the key state for an identifier + * @async + * @param {string} pre Identifier prefix + * @returns {Promise} A promise to the key states + */ + async get(pre: string): Promise { + const path = `/states?pre=${pre}`; + const data = null; + const method = 'GET'; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } + + /** + * Retrieve the key state for a list of identifiers + * @async + * @param {Array} pres List of identifier prefixes + * @returns {Promise} A promise to the key states + */ + async list(pres: string[]): Promise { + const path = `/states?${pres.map((pre) => `pre=${pre}`).join('&')}`; + const data = null; + const method = 'GET'; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } + + /** + * Query the key state of an identifier for a given sequence number or anchor + * @async + * @param {string} pre Identifier prefix + * @param {number} [sn] Optional sequence number + * @param {any} [anchor] Optional anchor + * @returns {Promise} A promise to the long-running operation + */ + async query(pre: string, sn?: string, anchor?: any): Promise { + const path = `/queries`; + const data: any = { + pre: pre, + }; + if (sn !== undefined) { + data.sn = sn; + } + if (anchor !== undefined) { + data.anchor = anchor; + } + + const method = 'POST'; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } +} + +export class Config { + public client: SignifyClient; + + /** + * Config + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + async get(): Promise { + const path = `/config`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } +} diff --git a/packages/signify-ts/src/keri/app/credentialing.ts b/packages/signify-ts/src/keri/app/credentialing.ts new file mode 100644 index 00000000..870f355e --- /dev/null +++ b/packages/signify-ts/src/keri/app/credentialing.ts @@ -0,0 +1,1001 @@ +import { SignifyClient } from './clienting.ts'; +import { interact, messagize } from '../core/eventing.ts'; +import { vdr } from '../core/vdring.ts'; +import { + b, + d, + Dict, + Protocols, + Ilks, + Serials, + versify, + Vrsn_1_0, +} from '../core/core.ts'; +import { Saider } from '../core/saider.ts'; +import { Serder } from '../core/serder.ts'; +import { Siger } from '../core/siger.ts'; +import { TraitDex } from './habery.ts'; +import { + serializeACDCAttachment, + serializeIssExnAttachment, +} from '../core/utils.ts'; +import { Operation } from './coring.ts'; +import { HabState } from '../core/keyState.ts'; + +import { components } from '../../types/keria-api-schema.ts'; + +export type CredentialResult = components['schemas']['Credential']; +export type Registry = components['schemas']['Registry']; +export type Schema = components['schemas']['Schema']; + +/** Types of credentials */ +export class CredentialTypes { + static issued = 'issued'; + static received = 'received'; +} + +/** Credential filter parameters */ +export interface CredentialFilter { + filter?: object; + sort?: object[]; + skip?: number; + limit?: number; +} + +export interface CredentialSubject { + /** + * Issuee, or holder of the credential. + */ + i?: string; + /** + * Timestamp of issuance. + */ + dt?: string; + /** + * Privacy salt + */ + u?: string; + [key: string]: unknown; +} + +export interface CredentialData { + v?: string; + d?: string; + /** + * Privacy salt + */ + u?: string; + /** + * Issuer of the credential. + */ + i?: string; + /** + * Registry id. + */ + ri?: string; + /** + * Schema id + */ + s?: string; + /** + * Credential subject data + */ + a: CredentialSubject; + /** + * Credential source section + */ + e?: { [key: string]: unknown }; + /** + * Credential rules section + */ + r?: { [key: string]: unknown }; +} + +export interface IssueCredentialResult { + acdc: Serder; + anc: Serder; + iss: Serder; + op: Operation; +} + +export interface RevokeCredentialResult { + anc: Serder; + rev: Serder; + op: Operation; +} + +export interface IpexApplyArgs { + /** + * Alias for the IPEX sender AID + */ + senderName: string; + + /** + * Prefix of the IPEX recipient AID + */ + recipient: string; + + /** + * Message to send + */ + message?: string; + + /** + * SAID of schema to apply for + */ + schemaSaid: string; + + /** + * Optional attributes for selective disclosure + */ + attributes?: Record; + datetime?: string; +} + +export interface IpexOfferArgs { + /** + * Alias for the IPEX sender AID + */ + senderName: string; + + /** + * Prefix of the IPEX recipient AID + */ + recipient: string; + + /** + * Message to send + */ + message?: string; + + /** + * ACDC to offer + */ + acdc: Serder; + + /** + * Optional qb64 SAID of apply message this offer is responding to + */ + applySaid?: string; + datetime?: string; +} + +export interface IpexAgreeArgs { + /** + * Alias for the IPEX sender AID + */ + senderName: string; + + /** + * Prefix of the IPEX recipient AID + */ + recipient: string; + + /** + * Message to send + */ + message?: string; + + /** + * qb64 SAID of offer message this agree is responding to + */ + offerSaid: string; + datetime?: string; +} + +export interface IpexGrantArgs { + /** + * Alias for the IPEX sender AID + */ + senderName: string; + + /** + * Prefix of the IPEX recipient AID + */ + recipient: string; + + /** + * Message to send + */ + message?: string; + + /** + * qb64 SAID of agree message this grant is responding to + */ + agreeSaid?: string; + datetime?: string; + acdc: Serder; + acdcAttachment?: string; + iss: Serder; + issAttachment?: string; + anc: Serder; + ancAttachment?: string; +} + +export interface IpexAdmitArgs { + /** + * Alias for the IPEX sender AID + */ + senderName: string; + + /** + * Prefix of the IPEX recipient AID + */ + recipient: string; + + /** + * Message to send + */ + message?: string; + + /** + * qb64 SAID of agree message this admit is responding to + */ + grantSaid: string; + datetime?: string; +} + +export type CredentialState = components['schemas']['CredentialState']; + +/** + * Credentials + */ +export class Credentials { + public client: SignifyClient; + /** + * Credentials + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * List credentials + * @async + * @param {CredentialFilter} [kargs] Optional parameters to filter the credentials + * @returns {Promise} A promise to the list of credentials + */ + async list(kargs: CredentialFilter = {}): Promise { + const path = `/credentials/query`; + const filtr = kargs.filter === undefined ? {} : kargs.filter; + const sort = kargs.sort === undefined ? [] : kargs.sort; + const limit = kargs.limit === undefined ? 25 : kargs.limit; + const skip = kargs.skip === undefined ? 0 : kargs.skip; + + const data = { + filter: filtr, + sort: sort, + skip: skip, + limit: limit, + }; + const method = 'POST'; + + const res = await this.client.fetch(path, method, data, undefined); + return await res.json(); + } + + /** + * Get a credential + * @async + * @param {string} said - SAID of the credential + * @param {boolean} [includeCESR=false] - Optional flag export the credential in CESR format + * @returns {Promise} A promise to the credential + */ + async get(said: string): Promise; + async get(said: string, includeCESR: false): Promise; + async get(said: string, includeCESR: true): Promise; + async get( + said: string, + includeCESR: boolean = false + ): Promise { + const path = `/credentials/${said}`; + const method = 'GET'; + const headers = includeCESR + ? new Headers({ Accept: 'application/json+cesr' }) + : new Headers({ Accept: 'application/json' }); + const res = await this.client.fetch(path, method, null, headers); + + return includeCESR ? await res.text() : await res.json(); + } + + /** + * Delete a credential from the DB + * @async + * @param {string} said - SAID of the credential + * @returns {Promise} + */ + async delete(said: string): Promise { + const path = `/credentials/${said}`; + const method = 'DELETE'; + await this.client.fetch(path, method, undefined); + } + + /** + * Get the state of a credential + * @async + * @param {string} ri - management registry identifier + * @param {string} said - SAID of the credential + * @returns {Promise} A promise to the credential registry state + */ + async state(ri: string, said: string): Promise { + const path = `/registries/${ri}/${said}`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return res.json(); + } + + /** + * Creates a credential in the specified registry to be GRANTed with IPEX to the intended recipient + */ + async issue( + name: string, + args: CredentialData + ): Promise { + const hab = await this.client.identifiers().get(name); + const estOnly = hab.state.c !== undefined && hab.state.c.includes('EO'); + if (estOnly) { + // TODO implement rotation event + throw new Error('Establishment only not implemented'); + } + if (!this.client.manager) { + throw new Error('No manager on client'); + } + + const keeper = this.client.manager.get(hab); + + const [, subject] = Saider.saidify({ + d: '', + ...args.a, + dt: args.a.dt ?? new Date().toISOString().replace('Z', '000+00:00'), + }); + + const [, acdc] = Saider.saidify({ + v: versify(Protocols.ACDC, undefined, Serials.JSON, 0), + d: '', + u: args.u, + i: args.i ?? hab.prefix, + ri: args.ri, + s: args.s, + a: subject, + e: args.e, + r: args.r, + }); + + const [, iss] = Saider.saidify({ + v: versify(Protocols.KERI, undefined, Serials.JSON, 0), + t: Ilks.iss, + d: '', + i: acdc.d, + s: '0', + ri: args.ri, + dt: subject.dt, + }); + + const sn = parseInt(hab.state.s, 16); + const anc = interact({ + pre: hab.prefix, + sn: sn + 1, + data: [ + { + i: iss.i, + s: iss.s, + d: iss.d, + }, + ], + dig: hab.state.d, + version: undefined, + kind: undefined, + }); + + const sigs = await keeper.sign(b(anc.raw)); + + const path = `/identifiers/${hab.name}/credentials`; + const method = 'POST'; + const body = { + acdc: acdc, + iss: iss, + ixn: anc.sad, + sigs, + [keeper.algo]: keeper.params(), + }; + + const res = await this.client.fetch(path, method, body); + const op = await res.json(); + + return { + acdc: new Serder(acdc), + iss: new Serder(iss), + anc, + op, + }; + } + + /** + * Revoke credential + * @async + * @param {string} name Name or alias of the identifier + * @param {string} said SAID of the credential + * @param {string} datetime date time of revocation + * @returns {Promise} A promise to the long-running operation + */ + async revoke( + name: string, + said: string, + datetime?: string + ): Promise { + const hab = await this.client.identifiers().get(name); + const pre: string = hab.prefix; + + const vs = versify(Protocols.KERI, undefined, Serials.JSON, 0); + const dt = + datetime ?? new Date().toISOString().replace('Z', '000+00:00'); + + const cred = await this.get(said); + + let registryId: string; + if ('ri' in cred.sad && cred.sad.ri !== undefined) { + registryId = cred.sad.ri; + } else if ('rd' in cred.sad && cred.sad.rd !== undefined) { + registryId = cred.sad.rd; + } else { + throw new Error('Neither ri nor rd property found in credential'); + } + + // Create rev + const _rev = { + v: vs, + t: Ilks.rev, + d: '', + i: said, + s: '1', + ri: registryId, + p: cred.status.d, + dt: dt, + }; + + const [, rev] = Saider.saidify(_rev); + + // create ixn + let ixn = {}; + let sigs = []; + + const state = hab.state; + if (state.c !== undefined && state.c.includes('EO')) { + var estOnly = true; + } else { + var estOnly = false; + } + + const sn = parseInt(state.s, 16); + const dig = state.d; + + const data: any = [ + { + i: rev.i, + s: rev.s, + d: rev.d, + }, + ]; + + const keeper = this.client!.manager!.get(hab); + + if (estOnly) { + // TODO implement rotation event + throw new Error('Establishment only not implemented'); + } else { + const serder = interact({ + pre: pre, + sn: sn + 1, + data: data, + dig: dig, + version: undefined, + kind: undefined, + }); + sigs = await keeper.sign(b(serder.raw)); + ixn = serder.sad; + } + + const body = { + rev: rev, + ixn: ixn, + sigs: sigs, + [keeper.algo]: keeper.params(), + }; + + const path = `/identifiers/${name}/credentials/${said}`; + const method = 'DELETE'; + const res = await this.client.fetch(path, method, body); + const op = await res.json(); + + return { + rev: new Serder(rev), + anc: new Serder(ixn), + op, + }; + } +} + +export interface CreateRegistryArgs { + name: string; + registryName: string; + toad?: string | number | undefined; + noBackers?: boolean; + baks?: string[]; + nonce?: string; +} + +export class RegistryResult { + private readonly _regser: any; + private readonly _serder: Serder; + private readonly _sigs: string[]; + private readonly promise: Promise; + + constructor( + regser: Serder, + serder: Serder, + sigs: any[], + promise: Promise + ) { + this._regser = regser; + this._serder = serder; + this._sigs = sigs; + this.promise = promise; + } + + get regser() { + return this._regser; + } + + get serder() { + return this._serder; + } + + get sigs() { + return this._sigs; + } + + async op(): Promise { + const res = await this.promise; + return await res.json(); + } +} + +/** + * Registries + */ +export class Registries { + public client: SignifyClient; + /** + * Registries + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * List registries + * @async + * @param {string} name Name or alias of the identifier + * @returns {Promise} A promise to the list of registries + */ + async list(name: string): Promise { + const path = `/identifiers/${name}/registries`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } + + /** + * Create a registry + * @async + * @param {CreateRegistryArgs} + * @returns {Promise<[any, Serder, any[], object]> } A promise to the long-running operation + */ + async create({ + name, + registryName, + noBackers = true, + toad = 0, + baks = [], + nonce, + }: CreateRegistryArgs): Promise { + const hab = await this.client.identifiers().get(name); + const pre: string = hab.prefix; + + const cnfg: string[] = []; + if (noBackers) { + cnfg.push(TraitDex.NoBackers); + } + + const state = hab.state; + const estOnly = state.c !== undefined && state.c.includes('EO'); + if (estOnly) { + cnfg.push(TraitDex.EstOnly); + } + + const regser = vdr.incept({ pre, baks, toad, nonce, cnfg }); + + if (estOnly) { + throw new Error('establishment only not implemented'); + } else { + const state = hab.state; + const sn = parseInt(state.s, 16); + const dig = state.d; + + const data: any = [ + { + i: regser.pre, + s: '0', + d: regser.pre, + }, + ]; + + const serder = interact({ + pre: pre, + sn: sn + 1, + data: data, + dig: dig, + version: Vrsn_1_0, + kind: Serials.JSON, + }); + const keeper = this.client.manager!.get(hab); + const sigs = await keeper.sign(b(serder.raw)); + const res = this.createFromEvents( + hab, + name, + registryName, + regser.sad, + serder.sad, + sigs + ); + return new RegistryResult(regser, serder, sigs, res); + } + } + + createFromEvents( + hab: HabState, + name: string, + registryName: string, + vcp: Dict, + ixn: Dict, + sigs: any[] + ) { + const path = `/identifiers/${name}/registries`; + const method = 'POST'; + + const data: any = { + name: registryName, + vcp: vcp, + ixn: ixn, + sigs: sigs, + }; + const keeper = this.client!.manager!.get(hab); + data[keeper.algo] = keeper.params(); + + return this.client.fetch(path, method, data); + } + + /** + * Rename a registry + * @async + * @param {string} name Name or alias of the identifier + * @param {string} registryName Current registry name + * @param {string} newName New registry name + * @returns {Promise} A promise to the registry record + */ + async rename( + name: string, + registryName: string, + newName: string + ): Promise { + const path = `/identifiers/${name}/registries/${registryName}`; + const method = 'PUT'; + const data = { + name: newName, + }; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } +} +/** + * Schemas + */ +export class Schemas { + client: SignifyClient; + /** + * Schemas + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Get a schema + * @async + * @param {string} said SAID of the schema + * @returns {Promise} A promise to the schema + */ + async get(said: string): Promise { + const path = `/schema/${said}`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } + + /** + * List schemas + * @async + * @returns {Promise} A promise to the list of schemas + */ + async list(): Promise { + const path = `/schema`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } +} + +/** + * Ipex + */ + +export class Ipex { + client: SignifyClient; + /** + * Schemas + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Create an IPEX apply EXN message + */ + async apply(args: IpexApplyArgs): Promise<[Serder, string[], string]> { + const hab = await this.client.identifiers().get(args.senderName); + const data = { + m: args.message ?? '', + s: args.schemaSaid, + a: args.attributes ?? {}, + }; + + return this.client + .exchanges() + .createExchangeMessage( + hab, + '/ipex/apply', + data, + {}, + args.recipient, + args.datetime, + undefined + ); + } + + async submitApply( + name: string, + exn: Serder, + sigs: string[], + recp: string[] + ): Promise { + const body = { + exn: exn.sad, + sigs, + rec: recp, + }; + + const response = await this.client.fetch( + `/identifiers/${name}/ipex/apply`, + 'POST', + body + ); + + return response.json(); + } + + /** + * Create an IPEX offer EXN message + */ + async offer(args: IpexOfferArgs): Promise<[Serder, string[], string]> { + const hab = await this.client.identifiers().get(args.senderName); + const data = { + m: args.message ?? '', + }; + + return this.client + .exchanges() + .createExchangeMessage( + hab, + '/ipex/offer', + data, + { acdc: [args.acdc, undefined] }, + args.recipient, + args.datetime, + args.applySaid + ); + } + + async submitOffer( + name: string, + exn: Serder, + sigs: string[], + atc: string, + recp: string[] + ): Promise { + const body = { + exn: exn.sad, + sigs, + atc, + rec: recp, + }; + + const response = await this.client.fetch( + `/identifiers/${name}/ipex/offer`, + 'POST', + body + ); + + return response.json(); + } + + /** + * Create an IPEX agree EXN message + */ + async agree(args: IpexAgreeArgs): Promise<[Serder, string[], string]> { + const hab = await this.client.identifiers().get(args.senderName); + const data = { + m: args.message ?? '', + }; + + return this.client + .exchanges() + .createExchangeMessage( + hab, + '/ipex/agree', + data, + {}, + args.recipient, + args.datetime, + args.offerSaid + ); + } + + async submitAgree( + name: string, + exn: Serder, + sigs: string[], + recp: string[] + ): Promise { + const body = { + exn: exn.sad, + sigs, + rec: recp, + }; + + const response = await this.client.fetch( + `/identifiers/${name}/ipex/agree`, + 'POST', + body + ); + + return response.json(); + } + + /** + * Create an IPEX grant EXN message + */ + async grant(args: IpexGrantArgs): Promise<[Serder, string[], string]> { + const hab = await this.client.identifiers().get(args.senderName); + const data = { + m: args.message ?? '', + }; + + let atc = args.ancAttachment; + if (atc === undefined) { + const keeper = this.client.manager!.get(hab); + const sigs = await keeper.sign(b(args.anc.raw)); + const sigers = sigs.map((sig: string) => new Siger({ qb64: sig })); + const ims = d(messagize(args.anc, sigers)); + atc = ims.substring(args.anc.size); + } + + const acdcAtc = + args.acdcAttachment === undefined + ? d(serializeACDCAttachment(args.iss)) + : args.acdcAttachment; + const issAtc = + args.issAttachment === undefined + ? d(serializeIssExnAttachment(args.anc)) + : args.issAttachment; + + const embeds: Record = { + acdc: [args.acdc, acdcAtc], + iss: [args.iss, issAtc], + anc: [args.anc, atc], + }; + + return this.client + .exchanges() + .createExchangeMessage( + hab, + '/ipex/grant', + data, + embeds, + args.recipient, + args.datetime, + args.agreeSaid + ); + } + + async submitGrant( + name: string, + exn: Serder, + sigs: string[], + atc: string, + recp: string[] + ): Promise { + const body = { + exn: exn.sad, + sigs: sigs, + atc: atc, + rec: recp, + }; + + const response = await this.client.fetch( + `/identifiers/${name}/ipex/grant`, + 'POST', + body + ); + + return response.json(); + } + + /** + * Create an IPEX admit EXN message + */ + async admit(args: IpexAdmitArgs): Promise<[Serder, string[], string]> { + const hab = await this.client.identifiers().get(args.senderName); + const data: any = { + m: args.message, + }; + + return this.client + .exchanges() + .createExchangeMessage( + hab, + '/ipex/admit', + data, + {}, + args.recipient, + args.datetime, + args.grantSaid + ); + } + + async submitAdmit( + name: string, + exn: Serder, + sigs: string[], + atc: string, + recp: string[] + ): Promise { + const body = { + exn: exn.sad, + sigs: sigs, + atc: atc, + rec: recp, + }; + + const response = await this.client.fetch( + `/identifiers/${name}/ipex/admit`, + 'POST', + body + ); + + return response.json(); + } +} diff --git a/packages/signify-ts/src/keri/app/delegating.ts b/packages/signify-ts/src/keri/app/delegating.ts new file mode 100644 index 00000000..cccb30d9 --- /dev/null +++ b/packages/signify-ts/src/keri/app/delegating.ts @@ -0,0 +1,33 @@ +import { EventResult } from './aiding.ts'; +import { SignifyClient } from './clienting.ts'; + +export class Delegations { + public client: SignifyClient; + /** + * Delegations + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Approve the delegation via interaction event + * @async + * @param {string} name Name or alias of the identifier + * @param {any} [data] The anchoring interaction event + * @returns {Promise} A promise to the delegated approval result + */ + async approve(name: string, data?: any): Promise { + const { serder, sigs, jsondata } = await this.client + .identifiers() + .createInteract(name, data); + + const res = await this.client.fetch( + '/identifiers/' + name + '/delegation', + 'POST', + jsondata + ); + return new EventResult(serder, sigs, res); + } +} diff --git a/packages/signify-ts/src/keri/app/escrowing.ts b/packages/signify-ts/src/keri/app/escrowing.ts new file mode 100644 index 00000000..34b75110 --- /dev/null +++ b/packages/signify-ts/src/keri/app/escrowing.ts @@ -0,0 +1,36 @@ +import { SignifyClient } from './clienting.ts'; +import { components } from '../../types/keria-api-schema.ts'; + +export type Rpy = components['schemas']['Rpy']; +/** + * Escrows + */ +export class Escrows { + client: SignifyClient; + + /** + * Escrows + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * List replay messages + * @async + * @param {string} [route] Optional route in the replay message + * @returns {Promise} A promise to the list of replay messages + */ + async listReply(route?: string): Promise { + const params = new URLSearchParams(); + if (route !== undefined) { + params.append('route', route); + } + + const path = `/escrows/rpy` + '?' + params.toString(); + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } +} diff --git a/packages/signify-ts/src/keri/app/exchanging.ts b/packages/signify-ts/src/keri/app/exchanging.ts new file mode 100644 index 00000000..94946649 --- /dev/null +++ b/packages/signify-ts/src/keri/app/exchanging.ts @@ -0,0 +1,223 @@ +import { SignifyClient } from './clienting.ts'; +import { b, d, Dict, Protocols, Ilks, Serials, versify } from '../core/core.ts'; +import { Serder } from '../core/serder.ts'; +import { nowUTC } from '../core/utils.ts'; +import { Pather } from '../core/pather.ts'; +import { Counter, CtrDex } from '../core/counter.ts'; +import { Saider } from '../core/saider.ts'; +import { HabState } from '../core/keyState.ts'; + +/** + * Exchanges + */ +export class Exchanges { + client: SignifyClient; + + /** + * Exchanges + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Create exn message + * @async + * @returns {Promise} A promise to the list of replay messages + * @param sender + * @param route + * @param payload + * @param embeds + * @param recipient + * @param datetime + * @param dig + */ + async createExchangeMessage( + sender: HabState, + route: string, + payload: Dict, + embeds: Dict, + recipient: string, + datetime?: string, + dig?: string + ): Promise<[Serder, string[], string]> { + const keeper = this.client.manager!.get(sender); + const [exn, end] = exchange( + route, + payload, + sender['prefix'], + recipient, + datetime, + dig, + undefined, + embeds + ); + + const sigs = await keeper.sign(b(exn.raw)); + return [exn, sigs, d(end)]; + } + + /** + * Send exn messages to list of recipients + * @async + * @returns {Promise} A promise to the list of replay messages + * @param name + * @param topic + * @param sender + * @param route + * @param payload + * @param embeds + * @param recipients + */ + async send( + name: string, + topic: string, + sender: HabState, + route: string, + payload: Dict, + embeds: Dict, + recipients: string[] + ): Promise { + for (const recipient of recipients) { + const [exn, sigs, atc] = await this.createExchangeMessage( + sender, + route, + payload, + embeds, + recipient + ); + return await this.sendFromEvents( + name, + topic, + exn, + sigs, + atc, + recipients + ); + } + } + + /** + * Send exn messaget to list of recipients + * @async + * @returns {Promise} A promise to the list of replay messages + * @param name + * @param topic + * @param exn + * @param sigs + * @param atc + * @param recipients + */ + async sendFromEvents( + name: string, + topic: string, + exn: Serder, + sigs: string[], + atc: string, + recipients: string[] + ): Promise { + const path = `/identifiers/${name}/exchanges`; + const method = 'POST'; + const data: any = { + tpc: topic, + exn: exn.sad, + sigs: sigs, + atc: atc, + rec: recipients, + }; + + const res = await this.client.fetch(path, method, data); + return await res.json(); + } + + /** + * Get exn message by said + * @async + * @returns A promise to the exn message + * @param said The said of the exn message + */ + async get(said: string): Promise { + const path = `/exchanges/${said}`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } +} + +export function exchange( + route: string, + payload: Dict, + sender: string, + recipient: string, + date?: string, + dig?: string, + modifiers?: Dict, + embeds?: Dict +): [Serder, Uint8Array] { + const vs = versify(Protocols.KERI, undefined, Serials.JSON, 0); + const ilk = Ilks.exn; + const dt = + date !== undefined + ? date + : nowUTC().toISOString().replace('Z', '000+00:00'); + const p = dig !== undefined ? dig : ''; + const q = modifiers !== undefined ? modifiers : {}; + const ems = embeds != undefined ? embeds : {}; + + let e = {} as Dict; + let end = ''; + Object.entries(ems).forEach(([key, value]) => { + const serder = value[0]; + const atc = value[1]; + e[key] = serder.sad; + + if (atc == undefined) { + return; + } + let pathed = ''; + const pather = new Pather({}, undefined, ['e', key]); + pathed += pather.qb64; + pathed += atc; + + const counter = new Counter({ + code: CtrDex.PathedMaterialQuadlets, + count: Math.floor(pathed.length / 4), + }); + end += counter.qb64; + end += pathed; + }); + + if (Object.keys(e).length > 0) { + e['d'] = ''; + [, e] = Saider.saidify(e); + } + + const attrs = {} as Dict; + + attrs['i'] = recipient; + + const a = { + ...attrs, + ...payload, + }; + + const _sad = { + v: vs, + t: ilk, + d: '', + i: sender, + rp: recipient, + p: p, + dt: dt, + r: route, + q: q, + a: a, + e: e, + }; + const [, sad] = Saider.saidify(_sad); + + const exn = new Serder(sad); + + return [exn, b(end)]; +} diff --git a/packages/signify-ts/src/keri/app/grouping.ts b/packages/signify-ts/src/keri/app/grouping.ts new file mode 100644 index 00000000..eef4b7c0 --- /dev/null +++ b/packages/signify-ts/src/keri/app/grouping.ts @@ -0,0 +1,90 @@ +import { SignifyClient } from './clienting.ts'; +import { Dict } from '../core/core.ts'; + +/** + * Groups + */ +export class Groups { + client: SignifyClient; + + /** + * Groups + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * Get group request messages + * @async + * @param {string} [said] SAID of exn message to load + * @returns {Promise} A promise to the list of replay messages + */ + async getRequest(said: string): Promise { + const path = `/multisig/request/` + said; + const method = 'GET'; + const res = await this.client.fetch(path, method, null); + return await res.json(); + } + + /** + * Send multisig exn request messages to other group members + * @async + * @param {string} [name] human readable name of group AID + * @param {Dict} [exn] exn message to send to other members + * @param {string[]} [sigs] signature of the participant over the exn + * @param {string} [atc] additional attachments from embedded events in exn + * @returns {Promise} A promise to the list of replay messages + */ + async sendRequest( + name: string, + exn: Dict, + sigs: string[], + atc: string + ): Promise { + const path = `/identifiers/${name}/multisig/request`; + const method = 'POST'; + const data = { + exn: exn, + sigs: sigs, + atc: atc, + }; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } + + /** + * Join multisig group using rotation event. + * This can be used by participants being asked to contribute keys to a rotation event to join an existing group. + * @async + * @param {string} [name] human readable name of group AID + * @param {any} [rot] rotation event + * @param {any} [sigs] signatures + * @param {string} [gid] prefix + * @param {string[]} [smids] array of particpants + * @param {string[]} [rmids] array of particpants + * @returns {Promise} A promise to the list of replay messages + */ + async join( + name: string, + rot: any, + sigs: any, //string[], + gid: string, + smids: string[], + rmids: string[] + ): Promise { + const path = `/identifiers/${name}/multisig/join`; + const method = 'POST'; + const data = { + tpc: 'multisig', + rot: rot.sad, + sigs: sigs, + gid: gid, + smids: smids, + rmids: rmids, + }; + const res = await this.client.fetch(path, method, data); + return await res.json(); + } +} diff --git a/packages/signify-ts/src/keri/app/habery.ts b/packages/signify-ts/src/keri/app/habery.ts new file mode 100644 index 00000000..eeacadfd --- /dev/null +++ b/packages/signify-ts/src/keri/app/habery.ts @@ -0,0 +1,185 @@ +import { Algos, Manager } from '../core/manager.ts'; +import { MtrDex } from '../core/matter.ts'; +import { Salter } from '../core/salter.ts'; +import { Verfer } from '../core/verfer.ts'; +import { Diger } from '../core/diger.ts'; +import { incept } from '../core/eventing.ts'; +import { Serder } from '../core/serder.ts'; + +export class TraitCodex { + EstOnly: string = 'EO'; // Only allow establishment events + DoNotDelegate: string = 'DND'; // Dot not allow delegated identifiers + NoBackers: string = 'NB'; // Do not allow backers +} + +export const TraitDex = new TraitCodex(); + +export interface HaberyArgs { + name: string; + passcode?: string; + seed?: string | undefined; + aeid?: string | undefined; + pidx?: number | undefined; + salt?: string | undefined; + tier?: string | undefined; +} + +export interface MakeHabArgs { + code?: string; + transferable?: boolean; + isith?: string; + icount?: number; + nsith?: string; + ncount?: number; + toad?: string | number; + wits?: Array; + delpre?: string; + estOnly?: boolean; + DnD?: boolean; + data?: any; +} + +export class Hab { + public name: string; + public serder: Serder; + + constructor(name: string, icp: Serder) { + this.name = name; + this.serder = icp; + } + + get pre(): string { + return this.serder.sad['i']; + } +} + +export class Habery { + private readonly _name: string; + private readonly _mgr: Manager; + private readonly _habs: Map = new Map(); + + constructor({ name, passcode, seed, aeid, pidx, salt }: HaberyArgs) { + this._name = name; + if (passcode != undefined && seed == undefined) { + if (passcode.length < 21) { + throw new Error('Bran (passcode seed material) too short.'); + } + + const bran = MtrDex.Salt_128 + 'A' + passcode.substring(0, 21); // qb64 salt for seed + const signer = new Salter({ qb64: bran }).signer( + MtrDex.Ed25519_Seed, + false + ); + seed = signer.qb64; + if (aeid == undefined) { + aeid = signer.verfer.qb64; // lest it remove encryption + } + } + let algo; + const salter = + salt != undefined ? new Salter({ qb64: salt }) : undefined; + if (salt != undefined) { + algo = Algos.salty; + } else { + algo = Algos.randy; + } + + this._mgr = new Manager({ + seed: seed, + aeid: aeid, + pidx: pidx, + algo: algo, + salter: salter, + }); + } + + get mgr(): Manager { + return this._mgr; + } + + get habs(): Array { + return Array.from(this._habs.values()); + } + + habByName(name: string): Hab | undefined { + return this._habs.get(name); + } + + makeHab( + name: string, + { + code = MtrDex.Blake3_256, + transferable = true, + isith = undefined, + icount = 1, + nsith = undefined, + ncount = undefined, + toad = undefined, + wits = undefined, + delpre = undefined, + estOnly = false, + DnD = false, + data = undefined, + }: MakeHabArgs + ): Hab { + if (nsith == undefined) { + nsith = isith; + } + if (ncount == undefined) { + ncount = icount; + } + if (!transferable) { + ncount = 0; + nsith = '0'; + code = MtrDex.Ed25519N; + } + + const [verfers, digers] = this._mgr.incept({ + icount: icount, + ncount: ncount, + stem: this.name, + transferable: transferable, + temp: false, + }); + + icount = verfers.length; + ncount = digers != undefined ? digers.length : 0; + if (isith == undefined) { + isith = `${Math.max(1, Math.ceil(icount / 2)).toString(16)}`; + } + if (nsith == undefined) { + nsith = `${Math.max(1, Math.ceil(ncount / 2)).toString(16)}`; + } + + const cnfg = new Array(); + if (estOnly) { + cnfg.push(TraitDex.EstOnly); + } + if (DnD) { + cnfg.push(TraitDex.DoNotDelegate); + } + + const keys = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + const ndigs = Array.from(digers, (diger: Diger) => diger.qb64); + + const icp = incept({ + keys, + isith, + ndigs, + nsith, + toad, + wits, + cnfg, + data, + code, + delpre, + }); + const hab = new Hab(name, icp); + this._habs.set(name, hab); + return hab; + } + + get name(): string { + return this._name; + } +} diff --git a/packages/signify-ts/src/keri/app/notifying.ts b/packages/signify-ts/src/keri/app/notifying.ts new file mode 100644 index 00000000..1b34b98f --- /dev/null +++ b/packages/signify-ts/src/keri/app/notifying.ts @@ -0,0 +1,69 @@ +import { SignifyClient } from './clienting.ts'; +import { parseRangeHeaders } from '../core/httping.ts'; + +/** + * Notifications + */ +export class Notifications { + client: SignifyClient; + + /** + * Notifications + * @param {SignifyClient} client + */ + constructor(client: SignifyClient) { + this.client = client; + } + + /** + * List notifications + * @async + * @param {number} [start=0] Start index of list of notifications, defaults to 0 + * @param {number} [end=24] End index of list of notifications, defaults to 24 + * @returns {Promise} A promise to the list of notifications + */ + async list(start: number = 0, end: number = 24): Promise { + const extraHeaders = new Headers(); + extraHeaders.append('Range', `notes=${start}-${end}`); + + const path = `/notifications`; + const method = 'GET'; + const res = await this.client.fetch(path, method, null, extraHeaders); + + const cr = res.headers.get('content-range'); + const range = parseRangeHeaders(cr, 'notes'); + const notes = await res.json(); + + return { + start: range.start, + end: range.end, + total: range.total, + notes: notes, + }; + } + + /** + * Mark a notification as read + * @async + * @param {string} said SAID of the notification + * @returns {Promise} A promise to the result of the marking + */ + async mark(said: string): Promise { + const path = `/notifications/` + said; + const method = 'PUT'; + const res = await this.client.fetch(path, method, null); + return await res.text(); + } + + /** + * Delete a notification + * @async + * @param {string} said SAID of the notification + * @returns {Promise} + */ + async delete(said: string): Promise { + const path = `/notifications/` + said; + const method = 'DELETE'; + await this.client.fetch(path, method, undefined); + } +} diff --git a/packages/signify-ts/src/keri/core/authing.ts b/packages/signify-ts/src/keri/core/authing.ts new file mode 100644 index 00000000..424979ea --- /dev/null +++ b/packages/signify-ts/src/keri/core/authing.ts @@ -0,0 +1,124 @@ +import { Signer } from './signer.ts'; +import { Verfer } from './verfer.ts'; +import { + desiginput, + HEADER_SIG_INPUT, + HEADER_SIG_TIME, + normalize, + siginput, +} from './httping.ts'; +import { Signage, signature, designature } from '../end/ending.ts'; +import { Cigar } from './cigar.ts'; +import { Siger } from './siger.ts'; +export class Authenticater { + static DefaultFields = [ + '@method', + '@path', + 'signify-resource', + HEADER_SIG_TIME.toLowerCase(), + ]; + private _verfer: Verfer; + private readonly _csig: Signer; + + constructor(csig: Signer, verfer: Verfer) { + this._csig = csig; + this._verfer = verfer; + } + + verify(headers: Headers, method: string, path: string): boolean { + const siginput = headers.get(HEADER_SIG_INPUT); + if (siginput == null) { + return false; + } + const signature = headers.get('Signature'); + if (signature == null) { + return false; + } + let inputs = desiginput(siginput); + inputs = inputs.filter((input) => input.name == 'signify'); + if (inputs.length == 0) { + return false; + } + inputs.forEach((input) => { + const items = new Array(); + input.fields!.forEach((field: string) => { + if (field.startsWith('@')) { + if (field == '@method') { + items.push(`"${field}": ${method}`); + } else if (field == '@path') { + items.push(`"${field}": ${path}`); + } + } else { + if (headers.has(field)) { + const value = normalize(headers.get(field) as string); + items.push(`"${field}": ${value}`); + } + } + }); + const values = new Array(); + values.push(`(${input.fields!.join(' ')})`); + values.push(`created=${input.created}`); + if (input.expires != undefined) { + values.push(`expires=${input.expires}`); + } + if (input.nonce != undefined) { + values.push(`nonce=${input.nonce}`); + } + if (input.keyid != undefined) { + values.push(`keyid=${input.keyid}`); + } + if (input.context != undefined) { + values.push(`context=${input.context}`); + } + if (input.alg != undefined) { + values.push(`alg=${input.alg}`); + } + const params = values.join(';'); + items.push(`"@signature-params: ${params}"`); + const ser = items.join('\n'); + const signage = designature(signature!); + const markers = signage[0].markers as Map; + const cig = markers.get(input.name); + if (!cig || !this._verfer.verify(cig.raw, ser)) { + throw new Error(`Signature for ${input.keyid} invalid.`); + } + }); + + return true; + } + + sign( + headers: Headers, + method: string, + path: string, + fields?: Array + ): Headers { + if (fields == undefined) { + fields = Authenticater.DefaultFields; + } + + const [header, sig] = siginput(this._csig, { + name: 'signify', + method, + path, + headers, + fields, + alg: 'ed25519', + keyid: this._csig.verfer.qb64, + }); + + header.forEach((value, key) => { + headers.append(key, value); + }); + + const markers = new Map(); + markers.set('signify', sig); + const signage = new Signage(markers, false); + const signed = signature([signage]); + signed.forEach((value, key) => { + headers.append(key, value); + }); + + return headers; + } +} diff --git a/packages/signify-ts/src/keri/core/base64.ts b/packages/signify-ts/src/keri/core/base64.ts new file mode 100644 index 00000000..2d98107e --- /dev/null +++ b/packages/signify-ts/src/keri/core/base64.ts @@ -0,0 +1,19 @@ +import { fromByteArray, toByteArray } from 'base64-js'; + +export function encodeBase64Url(input: Uint8Array): string { + return fromByteArray(input) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+/, ''); +} + +export function decodeBase64Url(input: string): Uint8Array { + if (!(typeof input === 'string')) { + throw new TypeError('`input` must be a string.'); + } + + const n = input.length % 4; + const padded = input + '='.repeat(n > 0 ? 4 - n : n); + const base64String = padded.replace(/-/g, '+').replace(/_/g, '/'); + return toByteArray(base64String); +} diff --git a/packages/signify-ts/src/keri/core/bexter.ts b/packages/signify-ts/src/keri/core/bexter.ts new file mode 100644 index 00000000..c29d223d --- /dev/null +++ b/packages/signify-ts/src/keri/core/bexter.ts @@ -0,0 +1,139 @@ +import { BexDex, Matter, MatterArgs, MtrDex } from './matter.ts'; +import { EmptyMaterialError } from './kering.ts'; +import { decodeBase64Url, encodeBase64Url } from './base64.ts'; +import { concat } from './core.ts'; + +const B64REX = '^[A-Za-z0-9\\-_]*$'; +export const Reb64 = new RegExp(B64REX); + +/* + + Bexter is subclass of Matter, cryptographic material, for variable length + strings that only contain Base64 URL safe characters, i.e. Base64 text (bext). + When created using the 'bext' paramaeter, the encoded matter in qb64 format + in the text domain is more compact than would be the case if the string were + passed in as raw bytes. The text is used as is to form the value part of the + qb64 version not including the leader. + + Due to ambiguity that arises from pre-padding bext whose length is a multiple of + three with one or more 'A' chars. Any bext that starts with an 'A' and whose length + is either a multiple of 3 or 4 may not round trip. Bext with a leading 'A' + whose length is a multiple of four may have the leading 'A' stripped when + round tripping. + + Bexter(bext='ABBB').bext == 'BBB' + Bexter(bext='BBB').bext == 'BBB' + Bexter(bext='ABBB').qb64 == '4AABABBB' == Bexter(bext='BBB').qb64 + + To avoid this problem, only use for applications of base 64 strings that + never start with 'A' + + Examples: base64 text strings: + + bext = "" + qb64 = '4AAA' + + bext = "-" + qb64 = '6AABAAA-' + + bext = "-A" + qb64 = '5AABAA-A' + + bext = "-A-" + qb64 = '4AABA-A-' + + bext = "-A-B" + qb64 = '4AAB-A-B' + + + Example uses: + CESR encoded paths for nested SADs and SAIDs + CESR encoded fractionally weighted threshold expressions + + + Attributes: + + Inherited Properties: (See Matter) + .pad is int number of pad chars given raw + + .code is str derivation code to indicate cypher suite + .raw is bytes crypto material only without code + .index is int count of attached crypto material by context (receipts) + .qb64 is str in Base64 fully qualified with derivation code + crypto mat + .qb64b is bytes in Base64 fully qualified with derivation code + crypto mat + .qb2 is bytes in binary with derivation code + crypto material + .transferable is Boolean, True when transferable derivation code False otherwise + + Properties: + .text is the Base64 text value, .qb64 with text code and leader removed. + + Hidden: + ._pad is method to compute .pad property + ._code is str value for .code property + ._raw is bytes value for .raw property + ._index is int value for .index property + ._infil is method to compute fully qualified Base64 from .raw and .code + ._exfil is method to extract .code and .raw from fully qualified Base64 + + Methods: + + + + + */ + +export class Bexter extends Matter { + constructor( + { raw, code = MtrDex.StrB64_L0, qb64b, qb64, qb2 }: MatterArgs, + bext?: string + ) { + if ( + raw === undefined && + qb64b === undefined && + qb64 === undefined && + qb2 === undefined + ) { + if (bext === undefined) + throw new EmptyMaterialError('Missing bext string.'); + + const match = Reb64.exec(bext); + if (!match) throw new Error('Invalid Base64.'); + + raw = Bexter._rawify(bext); + } + + super({ raw, code, qb64b, qb64, qb2 }); + + if (!BexDex.has(this.code)) + throw new Error(`Invalid code = ${this.code} for Bexter.`); + } + + static _rawify(bext: string): Uint8Array { + const ts = bext.length % 4; // bext size mod 4 + const ws = (4 - ts) % 4; // pre conv wad size in chars + const ls = (3 - ts) % 3; // post conv lead size in bytes + const wad = new Array(ws); + wad.fill('A'); + const base = wad.join('') + bext; // pre pad with wad of zeros in Base64 == 'A' + const raw = decodeBase64Url(base); // [ls:] // convert and remove leader + + return Uint8Array.from(raw).subarray(ls); // raw binary equivalent of text + } + + get bext(): string { + const sizage = Matter.Sizes.get(this.code); + const wad = Uint8Array.from(new Array(sizage?.ls).fill(0)); + const bext = encodeBase64Url(concat(wad, this.raw)); + + let ws = 0; + if (sizage?.ls === 0 && bext !== undefined) { + if (bext[0] === 'A') { + ws = 1; + } + } else { + ws = (sizage?.ls! + 1) % 4; + } + + return bext.substring(ws); + } +} diff --git a/packages/signify-ts/src/keri/core/cigar.ts b/packages/signify-ts/src/keri/core/cigar.ts new file mode 100644 index 00000000..4ed4cdc7 --- /dev/null +++ b/packages/signify-ts/src/keri/core/cigar.ts @@ -0,0 +1,18 @@ +import { Verfer } from './verfer.ts'; +import { Matter, MatterArgs } from './matter.ts'; + +export class Cigar extends Matter { + private _verfer: Verfer | undefined; + constructor({ raw, code, qb64, qb64b, qb2 }: MatterArgs, verfer?: Verfer) { + super({ raw, code, qb64, qb64b, qb2 }); + this._verfer = verfer; + } + + get verfer(): Verfer | undefined { + return this._verfer; + } + + set verfer(verfer: Verfer | undefined) { + this._verfer = verfer; + } +} diff --git a/packages/signify-ts/src/keri/core/cipher.ts b/packages/signify-ts/src/keri/core/cipher.ts new file mode 100644 index 00000000..667828b9 --- /dev/null +++ b/packages/signify-ts/src/keri/core/cipher.ts @@ -0,0 +1,34 @@ +import { Matter, MatterArgs, MtrDex } from './matter.ts'; +import { Decrypter } from './decrypter.ts'; + +export class Cipher extends Matter { + constructor({ raw, code, qb64, qb64b, qb2 }: MatterArgs) { + if (raw != undefined && code == undefined) { + if (raw.length == Matter._rawSize(MtrDex.X25519_Cipher_Salt)) { + code = MtrDex.X25519_Cipher_Salt; + } else if ( + raw.length == Matter._rawSize(MtrDex.X25519_Cipher_Seed) + ) { + code = MtrDex.X25519_Cipher_Salt; + } + } + super({ raw: raw, code: code, qb64b: qb64b, qb64: qb64, qb2: qb2 }); + + if ( + !Array.from([ + MtrDex.X25519_Cipher_Salt, + MtrDex.X25519_Cipher_Seed, + ]).includes(this.code) + ) { + throw new Error(`Unsupported Cipher code == ${this.code}`); + } + } + + decrypt( + prikey: Uint8Array | undefined = undefined, + seed: Uint8Array | undefined = undefined + ) { + const decrypter = new Decrypter({ qb64b: prikey }, seed); + return decrypter.decrypt(this.qb64b); + } +} diff --git a/packages/signify-ts/src/keri/core/core.ts b/packages/signify-ts/src/keri/core/core.ts new file mode 100644 index 00000000..e029190e --- /dev/null +++ b/packages/signify-ts/src/keri/core/core.ts @@ -0,0 +1,438 @@ +/** + * Serialization types supported by the KERI and ACDC protocols and this Signify implementation. + */ +export enum Serials { + JSON = 'JSON', +} + +/** + * Protocol types supported by the KERI and ACDC protocols and this Signify implementation. + */ +export enum Protocols { + KERI = 'KERI', + ACDC = 'ACDC', +} + +/** + * Represents a protocol version of the KERI, ACDC, or other protocol specified in a CESR version string. + */ +export class Version { + public major: number; + public minor: number; + + constructor(major: number = 1, minor: number = 0) { + this.major = major; + this.minor = minor; + } +} + +/** + * Denotes version 1.0 of a protocol. + */ +export const Vrsn_1_0 = new Version(); + +/** + * Types of KERI and ACDC events. + */ +export const Ilks = { + icp: 'icp', + rot: 'rot', + ixn: 'ixn', + dip: 'dip', + drt: 'drt', + rct: 'rct', + vrc: 'vrc', + rpy: 'rpy', + exn: 'exn', + vcp: 'vcp', + iss: 'iss', + rev: 'rev', + bis: 'bis', + brv: 'brv', +}; + +/** + * Field labels for an inception event in V1 of the KERI protocol. + */ +export const IcpLabels = [ + 'v', + 'i', + 's', + 't', + 'kt', + 'k', + 'n', + 'bt', + 'b', + 'c', + 'a', +]; + +/** + * Field labels for an delegated inception event in V1 of the KERI protocol. + */ +export const DipLabels = [ + 'v', + 'i', + 's', + 't', + 'kt', + 'k', + 'n', + 'bt', + 'b', + 'c', + 'a', + 'di', +]; + +/** + * Field labels for a rotation event in V1 of the KERI protocol. + */ +export const RotLabels = [ + 'v', + 'i', + 's', + 't', + 'p', + 'kt', + 'k', + 'n', + 'bt', + 'br', + 'ba', + 'a', +]; + +/** + * Field labels for an delegated rotation event in V1 of the KERI protocol. + */ +export const DrtLabels = [ + 'v', + 'i', + 's', + 't', + 'p', + 'kt', + 'k', + 'n', + 'bt', + 'br', + 'ba', + 'a', +]; + +/** + * Field labels for an interaction event in V1 of the KERI protocol. + */ +export const IxnLabels = ['v', 'i', 's', 't', 'p', 'a']; + +/** + * Field labels for a key state notice event in V1 of the KERI protocol. + */ +export const KsnLabels = [ + 'v', + 'i', + 's', + 't', + 'p', + 'd', + 'f', + 'dt', + 'et', + 'kt', + 'k', + 'n', + 'bt', + 'b', + 'c', + 'ee', + 'di', + 'r', +]; + +/** + * Field labels for a reply event in V1 of the KERI protocol. + */ +export const RpyLabels = ['v', 't', 'd', 'dt', 'r', 'a']; + +/** + * Full size of a CESR version string in bytes. + */ +export const VERFULLSIZE = 17; +/** + * Minimum number of bytes a CESR parser must sniff to receive the entire version string. + */ +export const MINSNIFFSIZE = 12 + VERFULLSIZE; +export const MINSIGSIZE = 4; + +// const version_pattern = 'KERI(?P[0-9a-f])(?P[0-9a-f]) +// (?P[A-Z]{4})(?P[0-9a-f]{6})' +// const version_pattern1 = `KERI\(\?P\[0\-9a\-f\]\)\(\?P\[0\-9a\-f\]\)\ +// (\?P\[A\-Z\]\{4\}\)\(\?P\[0\-9a\-f\]\{6\}\)_` + +/** + * Regular expression for a version 1 CESR object version string. + */ +export const VEREX = '(KERI|ACDC)([0-9a-f])([0-9a-f])([A-Z]{4})([0-9a-f]{6})_'; + +/** + * An interface for a basic dictionary type keyed by string with any value type. + * Mimics the Python dictionary type. + */ +export interface Dict { + [id: string]: TValue; +} + +/** + * Parses a serialization version string into the protocol, protocol version, serialization type, and raw size. + * Uses regex matchers to validate and extract version string parts. + * @param {string} versionString version string + * @return {Object} tuple of prototol (KERI or ACDC), kind of serialization like cbor,json, or mgpk, + * protocol version, and raw size of serialization + */ +export function deversify( + versionString: string +): [Protocols, Serials, Version, string] { + let kind; + let size; + let proto; + const version = Vrsn_1_0; + + // we need to identify how to match the buffers pattern ,like we do regex matching for strings + const re = new RegExp(VEREX); + + // Regex pattern matching + const match = re.exec(versionString); + + if (match) { + [proto, version.major, version.minor, kind, size] = [ + match[1], + +match[2], + +match[3], + match[4], + match[5], + ]; + if (!Object.values(Serials).includes(kind as Serials)) { + throw new Error(`Invalid serialization kind = ${kind}`); + } + if (!Object.values(Protocols).includes(proto as Protocols)) { + throw new Error(`Invalid serialization kind = ${kind}`); + } + + const ta = kind as keyof typeof Serials; + kind = Serials[ta]; + const pa = proto as keyof typeof Protocols; + proto = Protocols[pa]; + + return [proto, kind, version, size]; + } + throw new Error(`Invalid version string = ${versionString}`); +} + +/** + * Returns a valid KERI serialization version string specifying the protocol, + * protocol version, serialization type, and raw byte size of the serialization. + * + * Defaults to version 1.0. + * @param ident + * @param version + * @param kind + * @param size + */ +export function versify( + ident: Protocols = Protocols.KERI, + version?: Version, + kind: Serials = Serials.JSON, + size: number = 0 +) { + version = version == undefined ? Vrsn_1_0 : version; // defaults to Version 1 + const major = version.major.toString(16); // hex digits + const minor = version.minor.toString(16); // hex digits + // raw size in hex digits zero padded to 6 characters + const rawSize = size.toString(16).padStart(6, '0'); + const terminationChar = '_'; // v1 termination character + return `${ident}${major}${minor}${kind}${rawSize}${terminationChar}`; +} + +/** + * Map allowing lookup of Base64URLSafe characters by index. + */ +export const B64ChrByIdx = new Map([ + [0, 'A'], + [1, 'B'], + [2, 'C'], + [3, 'D'], + [4, 'E'], + [5, 'F'], + [6, 'G'], + [7, 'H'], + [8, 'I'], + [9, 'J'], + [10, 'K'], + [11, 'L'], + [12, 'M'], + [13, 'N'], + [14, 'O'], + [15, 'P'], + [16, 'Q'], + [17, 'R'], + [18, 'S'], + [19, 'T'], + [20, 'U'], + [21, 'V'], + [22, 'W'], + [23, 'X'], + [24, 'Y'], + [25, 'Z'], + [26, 'a'], + [27, 'b'], + [28, 'c'], + [29, 'd'], + [30, 'e'], + [31, 'f'], + [32, 'g'], + [33, 'h'], + [34, 'i'], + [35, 'j'], + [36, 'k'], + [37, 'l'], + [38, 'm'], + [39, 'n'], + [40, 'o'], + [41, 'p'], + [42, 'q'], + [43, 'r'], + [44, 's'], + [45, 't'], + [46, 'u'], + [47, 'v'], + [48, 'w'], + [49, 'x'], + [50, 'y'], + [51, 'z'], + [52, '0'], + [53, '1'], + [54, '2'], + [55, '3'], + [56, '4'], + [57, '5'], + [58, '6'], + [59, '7'], + [60, '8'], + [61, '9'], + [62, '-'], + [63, '_'], +]); + +/** + * Map allowing lookup of Base64URLSafe index by character. + */ +export const B64IdxByChr = new Map( + Array.from(B64ChrByIdx, (entry) => [entry[1], entry[0]]) +); + +/** + * Converts an integer to a Base64URLSafe encoded string, left padded as specified. + * @param i integer to convert + * @param l minimum length of Base64 digits left padded with Base64 0 == 'A' character. + */ +export function intToB64(i: number, l = 1): string { + let out = ''; + while (l != 0) { + out = B64ChrByIdx.get(i % 64) + out; + i = Math.floor(i / 64); + if (i == 0) { + break; + } + } + + const x = l - out.length; + for (let i = 0; i < x; i++) { + out = 'A' + out; + } + + return out; +} + +/** + * Converts an integer to a Base64URLSafe encoded string to a byte array, left padded as specified. + * @param i integer to convert + * @param l minimum length of Base64 digits left padded with Base64 0 == 'A' character. + */ +export function intToB64b(n: number, l: number = 1): Uint8Array { + const s = intToB64(n, l); + return b(s); +} + +/** + * Converts a Base64URLSafe encoded string to an integer. + * @param s string to convert + */ +export function b64ToInt(s: string): number { + if (s.length == 0) { + throw new Error('Empty string, conversion undefined.'); + } + + let i = 0; + const rev = s.split('').reverse(); + rev.forEach((c: string, e: number) => { + i |= B64IdxByChr.get(c)! << (e * 6); + }); + + return i; +} + +// Built in encoder and decoder for converting to and from UTF-8 strings and Uint8Array byte arrays. +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +/** + * Converts a UTF-8 string to bytes. + * Output is an encoded array of bytes. Assumes UTF-8 encoding. + * @param s string to be encoded as an array of bytes + */ +export function b(s?: string): Uint8Array { + return encoder.encode(s); +} + +/** + * Convert bytes to UTF-8 string. + * @param u array of bytes to be converted to UTF-8 string. + */ +export function d(u?: Uint8Array): string { + return decoder.decode(u); +} + +/** + * Concatenates two byte arrays together in a new byte array. + * @param one first byte array to be concatenated + * @param two second byte array to be concatenated + */ +export function concat(one: Uint8Array, two: Uint8Array): Uint8Array { + const out = new Uint8Array(one.length + two.length); + out.set(one); + out.set(two, one.length); + return out; +} + +/** + * Converts a big-endian byte array into an integer. + * + * @param array - A `Uint8Array` of bytes representing a big-endian integer. + * @returns The integer represented by the byte array. + * + * Example: + * readInt(Uint8Array([0x01, 0x02, 0x03])) // returns 66051 + * + * How it works: + * - The function interprets the array as a big-endian number. + * - Each byte is added to the integer after shifting the previous value left by 8 bits (multiplying by 256). + */ +export function readInt(array: Uint8Array) { + let value = 0; + for (let i = 0; i < array.length; i++) { + value = value * 256 + array[i]; + } + return value; +} diff --git a/packages/signify-ts/src/keri/core/counter.ts b/packages/signify-ts/src/keri/core/counter.ts new file mode 100644 index 00000000..8c80332e --- /dev/null +++ b/packages/signify-ts/src/keri/core/counter.ts @@ -0,0 +1,273 @@ +import { Codex, Sizage } from './matter.ts'; +import { b, b64ToInt, d, intToB64 } from './core.ts'; + +export interface CounterArgs { + code?: string; + count?: number; + countB64?: string; + qb64b?: Uint8Array; + qb64?: string; + qb2?: Uint8Array; + strip?: boolean; +} + +export class CounterCodex extends Codex { + public ControllerIdxSigs: string = '-A'; // Qualified Base64 Indexed Signature. + public WitnessIdxSigs: string = '-B'; // Qualified Base64 Indexed Signature. + public NonTransReceiptCouples: string = '-C'; // Composed Base64 Couple, pre+cig. + public TransReceiptQuadruples: string = '-D'; // Composed Base64 Quadruple, pre+snu+dig+sig. + public FirstSeenReplayCouples: string = '-E'; // Composed Base64 Couple, fnu+dts. + public TransIdxSigGroups: string = '-F'; // Composed Base64 Group, pre+snu+dig+ControllerIdxSigs group. + public SealSourceCouples: string = '-G'; // Composed Base64 couple, snu+dig of given delegators or issuers event + public TransLastIdxSigGroups: string = '-H'; // Composed Base64 Group, pre+ControllerIdxSigs group. + public SealSourceTriples: string = '-I'; // Composed Base64 triple, pre+snu+dig of anchoring source event + public SadPathSig: string = '-J'; // Composed Base64 Group path+TransIdxSigGroup of SAID of content + public SadPathSigGroup: string = '-K'; // Composed Base64 Group, root(path)+SaidPathCouples + public PathedMaterialQuadlets: string = '-L'; // Composed Grouped Pathed Material Quadlet (4 char each) + public AttachedMaterialQuadlets: string = '-V'; // Composed Grouped Attached Material Quadlet (4 char each) + public BigAttachedMaterialQuadlets: string = '-0V'; // Composed Grouped Attached Material Quadlet (4 char each) + public KERIProtocolStack: string = '--AAA'; // KERI ACDC Protocol Stack CESR Version +} + +export const CtrDex = new CounterCodex(); + +export class Counter { + static Sizes = new Map( + Object.entries({ + '-A': new Sizage(2, 2, 4, 0), + '-B': new Sizage(2, 2, 4, 0), + '-C': new Sizage(2, 2, 4, 0), + '-D': new Sizage(2, 2, 4, 0), + '-E': new Sizage(2, 2, 4, 0), + '-F': new Sizage(2, 2, 4, 0), + '-G': new Sizage(2, 2, 4, 0), + '-H': new Sizage(2, 2, 4, 0), + '-I': new Sizage(2, 2, 4, 0), + '-J': new Sizage(2, 2, 4, 0), + '-K': new Sizage(2, 2, 4, 0), + '-L': new Sizage(2, 2, 4, 0), + '-V': new Sizage(2, 2, 4, 0), + '-0V': new Sizage(3, 5, 8, 0), + '--AAA': new Sizage(5, 3, 8, 0), + }) + ); + + static Hards = new Map([ + ['-A', 2], + ['-B', 2], + ['-C', 2], + ['-D', 2], + ['-E', 2], + ['-F', 2], + ['-G', 2], + ['-H', 2], + ['-I', 2], + ['-J', 2], + ['-K', 2], + ['-L', 2], + ['-M', 2], + ['-N', 2], + ['-O', 2], + ['-P', 2], + ['-Q', 2], + ['-R', 2], + ['-S', 2], + ['-T', 2], + ['-U', 2], + ['-V', 2], + ['-W', 2], + ['-X', 2], + ['-Y', 2], + ['-Z', 2], + ['-a', 2], + ['-b', 2], + ['-c', 2], + ['-d', 2], + ['-e', 2], + ['-f', 2], + ['-g', 2], + ['-h', 2], + ['-i', 2], + ['-j', 2], + ['-k', 2], + ['-l', 2], + ['-m', 2], + ['-n', 2], + ['-o', 2], + ['-p', 2], + ['-q', 2], + ['-r', 2], + ['-s', 2], + ['-t', 2], + ['-u', 2], + ['-v', 2], + ['-w', 2], + ['-x', 2], + ['-y', 2], + ['-z', 2], + ['-0', 3], + ['--', 5], + ]); + + private _code: string = ''; + private _count: number = -1; + + constructor({ code, count, countB64, qb64b, qb64, qb2 }: CounterArgs) { + if (code != undefined) { + if (!Counter.Sizes.has(code)) { + throw new Error(`"Unsupported code=${code}.`); + } + + const sizage = Counter.Sizes.get(code)!; + const cs = sizage.hs + sizage.ss; + if (sizage.fs != cs || cs % 4 != 0) { + throw new Error( + `Whole code size not full size or not multiple of 4. cs=${cs} fs=${sizage.fs}.` + ); + } + + if (count == undefined) { + count = countB64 == undefined ? 1 : b64ToInt(countB64); + } + + if (count < 0 || count > 64 ** sizage.ss - 1) { + throw new Error(`Invalid count=${count} for code=${code}.`); + } + + this._code = code; + this._count = count; + } else if (qb64b != undefined) { + const qb64 = d(qb64b); + this._exfil(qb64); + } else if (qb64 != undefined) { + this._exfil(qb64); + } else if (qb2 != undefined) { + } else { + throw new Error( + `Improper initialization need either (code and count) or qb64b or qb64 or qb2.` + ); + } + } + + get code(): string { + return this._code; + } + + get count() { + return this._count; + } + + get qb64() { + return this._infil(); + } + + get qb64b() { + return b(this.qb64); + } + + countToB64(l?: number): string { + if (l == undefined) { + const sizage = Counter.Sizes.get(this.code)!; + l = sizage.ss; + } + return intToB64(this.count, l); + } + + static semVerToB64( + version: string = '', + major: number = 0, + minor: number = 0, + patch: number = 0 + ): string { + let parts = [major, minor, patch]; + if (version != '') { + const ssplits = version.split('.'); + const splits = ssplits.map((x) => { + if (x == '') return 0; + return parseInt(x); + }); + + const off = splits.length; + const x = 3 - off; + for (let i = 0; i < x; i++) { + splits.push(parts[i + off]); + } + parts = splits; + } + + parts.forEach((p) => { + if (p < 0 || p > 63) { + throw new Error( + `Out of bounds semantic version. Part=${p} is < 0 or > 63.` + ); + } + }); + + return parts + .map((p) => { + return intToB64(p, 1); + }) + .join(''); + } + + private _infil(): string { + const code = this.code; + const count = this.count; + + const sizage = Counter.Sizes.get(code)!; + const cs = sizage.hs + sizage.ss; + if (sizage.fs != cs || cs % 4 != 0) { + throw new Error( + `Whole code size not full size or not multiple of 4. cs=${cs} fs=${sizage.fs}.` + ); + } + + if (count < 0 || count > 64 ** sizage.ss - 1) { + throw new Error(`Invalid count=${count} for code=${code}.`); + } + + const both = `${code}${intToB64(count, sizage.ss)}`; + + if (both.length % 4) { + throw new Error( + `Invalid size = ${both.length} of ${both} not a multiple of 4.` + ); + } + + return both; + } + + private _exfil(qb64: string) { + if (qb64.length == 0) { + throw new Error('Empty Material'); + } + + const first = qb64.slice(0, 2); + if (!Counter.Hards.has(first)) { + throw new Error(`Unexpected code ${first}`); + } + + const hs = Counter.Hards.get(first)!; + if (qb64.length < hs) { + throw new Error(`Need ${hs - qb64.length} more characters.`); + } + + const hard = qb64.slice(0, hs); + if (!Counter.Sizes.has(hard)) { + throw new Error(`Unsupported code ${hard}`); + } + + const sizage = Counter.Sizes.get(hard)!; + const cs = sizage!.hs + sizage!.ss; + + if (qb64.length < cs) { + throw new Error(`Need ${cs - qb64.length} more chars.`); + } + + const scount = qb64.slice(sizage.hs, sizage.hs + sizage.ss); + const count = b64ToInt(scount); + + this._code = hard; + this._count = count; + } +} diff --git a/packages/signify-ts/src/keri/core/decrypter.ts b/packages/signify-ts/src/keri/core/decrypter.ts new file mode 100644 index 00000000..4e5f04db --- /dev/null +++ b/packages/signify-ts/src/keri/core/decrypter.ts @@ -0,0 +1,80 @@ +import libsodium from 'libsodium-wrappers-sumo'; + +import { Matter, MatterArgs, MtrDex } from './matter.ts'; +import { Signer } from './signer.ts'; +import { Cipher } from './cipher.ts'; +import { EmptyMaterialError } from './kering.ts'; +import { Salter } from './salter.ts'; + +export class Decrypter extends Matter { + private readonly _decrypt: any; + constructor( + { raw, code = MtrDex.X25519_Private, qb64, qb64b, qb2 }: MatterArgs, + seed: Uint8Array | undefined = undefined + ) { + try { + super({ raw, code, qb64, qb64b, qb2 }); + } catch (e) { + if (e instanceof EmptyMaterialError) { + if (seed != undefined) { + const signer = new Signer({ qb64b: seed }); + if (signer.code != MtrDex.Ed25519_Seed) { + throw new Error( + `Unsupported signing seed derivation code ${signer.code}` + ); + } + const sigkey = new Uint8Array( + signer.raw.length + signer.verfer.raw.length + ); + sigkey.set(signer.raw); + sigkey.set(signer.verfer.raw, signer.raw.length); + raw = + libsodium.crypto_sign_ed25519_sk_to_curve25519(sigkey); + super({ raw, code, qb64, qb64b, qb2 }); + } else { + throw e; + } + } else { + throw e; + } + } + + if (this.code == MtrDex.X25519_Private) { + this._decrypt = this._x25519; + } else { + throw new Error(`Unsupported decrypter code = ${this.code}.`); + } + } + + decrypt( + ser: Uint8Array | null = null, + cipher: Cipher | null = null, + transferable: boolean = false + ) { + if (ser == null && cipher == null) { + throw new EmptyMaterialError('Neither ser or cipher were provided'); + } + + if (ser != null) { + cipher = new Cipher({ qb64b: ser }); + } + + return this._decrypt(cipher, this.raw, transferable); + } + + _x25519(cipher: Cipher, prikey: Uint8Array, transferable: boolean = false) { + const pubkey = libsodium.crypto_scalarmult_base(prikey); + const plain = libsodium.crypto_box_seal_open( + cipher.raw, + pubkey, + prikey + ); + if (cipher.code == MtrDex.X25519_Cipher_Salt) { + return new Salter({ qb64b: plain }); + } else if (cipher.code == MtrDex.X25519_Cipher_Seed) { + return new Signer({ qb64b: plain, transferable: transferable }); + } else { + throw new Error(`Unsupported cipher text code == ${cipher.code}`); + } + } +} diff --git a/packages/signify-ts/src/keri/core/diger.ts b/packages/signify-ts/src/keri/core/diger.ts new file mode 100644 index 00000000..37556b6e --- /dev/null +++ b/packages/signify-ts/src/keri/core/diger.ts @@ -0,0 +1,85 @@ +import { blake3 } from '@noble/hashes/blake3'; +import { Matter, MatterArgs, MtrDex } from './matter.ts'; + +/** + * @description : Diger is subset of Matter and is used to verify the digest of serialization + * It uses .raw : as digest + * .code as digest algorithm + * + */ + +export class Diger extends Matter { + private readonly _verify: (a: Uint8Array, b: Uint8Array) => boolean; + + // This constructor will assign digest verification function to ._verify + constructor( + { raw, code = MtrDex.Blake3_256, qb64, qb64b, qb2 }: MatterArgs, + ser: Uint8Array | null = null + ) { + try { + super({ raw, code, qb64, qb64b, qb2 }); + } catch (error) { + if (ser == null) { + throw error; + } + + if (code === MtrDex.Blake3_256) { + const dig = blake3.create({ dkLen: 32 }).update(ser).digest(); + super({ raw: dig, code: code }); + } else { + throw new Error(`Unsupported code = ${code} for digester.`); + } + } + + if (code === MtrDex.Blake3_256) { + this._verify = this.blake3_256; + } else { + throw new Error(`Unsupported code = ${code} for digester.`); + } + } + + /** + * + * @param {Uint8Array} ser serialization bytes + * @description This method will return true if digest of bytes serialization ser matches .raw + * using .raw as reference digest for ._verify digest algorithm determined + by .code + */ + verify(ser: Uint8Array): boolean { + return this._verify(ser, this.raw); + } + + compare( + ser: Uint8Array, + dig: Uint8Array | null = null, + diger: Diger | null = null + ) { + if (dig != null) { + if (dig.toString() == this.qb64) { + return true; + } + + diger = new Diger({ qb64b: dig }); + } else if (diger != null) { + if (diger.qb64b == this.qb64b) { + return true; + } + } else { + throw new Error('Both dig and diger may not be None.'); + } + + if (diger.code == this.code) { + return false; + } + + return diger.verify(ser) && this.verify(ser); + } + + blake3_256(ser: Uint8Array, dig: Uint8Array) { + const digest = blake3.create({ dkLen: 32 }).update(ser).digest(); + + return ( + digest.length == dig.length && digest.toString() === dig.toString() + ); + } +} diff --git a/packages/signify-ts/src/keri/core/encrypter.ts b/packages/signify-ts/src/keri/core/encrypter.ts new file mode 100644 index 00000000..825b9fc0 --- /dev/null +++ b/packages/signify-ts/src/keri/core/encrypter.ts @@ -0,0 +1,70 @@ +import libsodium from 'libsodium-wrappers-sumo'; + +import { Matter, MatterArgs, MtrDex } from './matter.ts'; +import { Verfer } from './verfer.ts'; +import { Signer } from './signer.ts'; +import { Cipher } from './cipher.ts'; +import { arrayEquals } from './utils.ts'; + +export class Encrypter extends Matter { + private _encrypt: any; + constructor( + { raw, code = MtrDex.X25519, qb64, qb64b, qb2 }: MatterArgs, + verkey: Uint8Array | null = null + ) { + if (raw == undefined && verkey != null) { + const verfer = new Verfer({ qb64b: verkey }); + if ( + !Array.from([MtrDex.Ed25519N, MtrDex.Ed25519]).includes( + verfer.code + ) + ) { + throw new Error( + `Unsupported verkey derivation code = ${verfer.code}.` + ); + } + raw = libsodium.crypto_sign_ed25519_pk_to_curve25519(verfer.raw); + } + + super({ raw, code, qb64, qb64b, qb2 }); + + if (this.code == MtrDex.X25519) { + this._encrypt = this._x25519; + } else { + throw new Error(`Unsupported encrypter code = ${this.code}.`); + } + } + + verifySeed(seed: Uint8Array) { + const signer = new Signer({ qb64b: seed }); + const keypair = libsodium.crypto_sign_seed_keypair(signer.raw); + const pubkey = libsodium.crypto_sign_ed25519_pk_to_curve25519( + keypair.publicKey + ); + return arrayEquals(pubkey, this.raw); + } + + encrypt(ser: Uint8Array | null = null, matter: Matter | null = null) { + if (ser == null && matter == null) { + throw new Error('Neither ser nor matter are provided.'); + } + + if (ser != null) { + matter = new Matter({ qb64b: ser }); + } + + let code; + if (matter!.code == MtrDex.Salt_128) { + code = MtrDex.X25519_Cipher_Salt; + } else { + code = MtrDex.X25519_Cipher_Seed; + } + + return this._encrypt(matter!.qb64, this.raw, code); + } + + _x25519(ser: Uint8Array, pubkey: Uint8Array, code: string) { + const raw = libsodium.crypto_box_seal(ser, pubkey); + return new Cipher({ raw: raw, code: code }); + } +} diff --git a/packages/signify-ts/src/keri/core/eventing.ts b/packages/signify-ts/src/keri/core/eventing.ts new file mode 100644 index 00000000..6d14101d --- /dev/null +++ b/packages/signify-ts/src/keri/core/eventing.ts @@ -0,0 +1,585 @@ +import { + b, + concat, + Dict, + Protocols, + Ilks, + Serials, + versify, + Version, + Vrsn_1_0, +} from './core.ts'; +import { Tholder } from './tholder.ts'; +import { CesrNumber } from './number.ts'; +import { Prefixer } from './prefixer.ts'; +import { Serder } from './serder.ts'; +import { MtrDex, NonTransDex } from './matter.ts'; +import { Saider } from './saider.ts'; +import { Siger } from './siger.ts'; +import { Cigar } from './cigar.ts'; +import { Counter, CtrDex } from './counter.ts'; +import { Seqner } from './seqner.ts'; + +const MaxIntThold = 2 ** 32 - 1; + +export interface RotateArgs { + pre?: string; + keys: Array; + dig?: string; + ilk?: string; + sn?: number; + isith?: number | string | Array; + ndigs?: Array; + nsith?: number | string | Array; + toad?: number; + wits?: Array; + cuts?: Array; + adds?: Array; + cnfg?: Array; + data?: Array; + version?: Version; + kind?: Serials; + size?: number; + intive?: boolean; +} + +export function rotate({ + pre = undefined, + keys, + dig = undefined, + ilk = Ilks.rot, + sn = 1, + isith = undefined, + ndigs = undefined, + nsith = undefined, + wits = undefined, + cuts = undefined, + adds = undefined, + toad = undefined, + data = undefined, + version = undefined, + kind = undefined, + intive = true, +}: RotateArgs) { + const vs = versify(Protocols.KERI, version, kind, 0); + const _ilk = ilk; + if (_ilk != Ilks.rot && _ilk != Ilks.drt) { + throw new Error(`Invalid ilk = ${ilk} for rot or drt.`); + } + + const sner = new CesrNumber({}, sn); + if (sner.num < 1) { + throw new Error(`Invalid sn = 0x${sner.numh} for rot or drt.`); + } + let _isit: number; + + if (isith == undefined) { + _isit = Math.max(1, Math.ceil(keys.length / 2)); + } else { + _isit = isith as number; // TODO this type as number does not make sense when isith is a string containing weighted thresholds + } + + const tholder = new Tholder({ sith: _isit }); + if (tholder.num != undefined && tholder.num < 1) { + throw new Error(`Invalid sith = ${tholder.num} less than 1.`); + } + if (tholder.size > keys.length) { + // TODO this error should say that the threshold has not been met + throw new Error(`Invalid sith = ${tholder.num} for keys = ${keys}`); + } + + let _ndigs: Array; + + if (ndigs === undefined) { + _ndigs = []; + } else { + _ndigs = ndigs; + } + + let _nsith; + if (nsith === undefined) { + _nsith = Math.max(1, Math.ceil(_ndigs.length / 2)); + } else { + _nsith = nsith; + } + + const ntholder = new Tholder({ sith: _nsith }); + if (ntholder.num != undefined && ntholder.num < 1) { + throw new Error(`Invalid sith = ${ntholder.num} less than 1.`); + } + if (ntholder.size > _ndigs.length) { + // TODO this error should say that the threshold has not been met + throw new Error( + `Signing threshold failure: ${keys.length} number of signers not equal to or greater than sith = ${tholder.size} for keys = ${keys}` + ); + } + + let _wits: Array; + if (wits === undefined) { + _wits = []; + } else { + _wits = wits; + } + const witset = new Set(_wits); + if (witset.size != _wits.length) { + throw new Error(`Invalid wits = ${wits}, has duplicates.`); + } + + let _cuts: Array; + if (cuts === undefined) { + _cuts = []; + } else { + _cuts = cuts; + } + const cutset = new Set(_cuts); + if (cutset.size != _cuts.length) { + throw new Error(`Invalid cuts = ${cuts}, has duplicates.`); + } + + let _adds: Array; + if (adds === undefined) { + _adds = []; + } else { + _adds = adds; + } + const addset = new Set(_adds); + + //non empty intersection of witset and addset + const witaddset = new Set([...witset].filter((x) => addset.has(x))); + if (witaddset.size > 0) { + throw new Error( + `Invalid member combination among wits = ${wits}, and adds = ${adds}.` + ); + } + + // non empty intersection of cutset and addset + const cutaddset = new Set([...cutset].filter((x) => addset.has(x))); + if (cutaddset.size > 0) { + throw new Error( + `Invalid member combination among cuts = ${cuts}, and adds = ${adds}.` + ); + } + + const newitsetdiff = new Set(_wits); + _cuts.forEach(function (v) { + newitsetdiff.delete(v); + }); + const newitset = new Set( + (function* () { + yield* newitsetdiff; + yield* addset; + })() + ); + + if (newitset.size != witset.size - cutset.size + addset.size) { + throw new Error( + `Invalid member combination among wits = ${wits}, cuts = ${cuts}, and adds = ${adds}.` + ); + } + + let _toad: number; + + if (toad === undefined) { + if (newitset.size == 0) { + _toad = 0; + } else { + _toad = ample(newitset.size); + } + } else { + _toad = toad; + } + + if (newitset.size > 0) { + if (_toad < 1 || _toad > newitset.size) { + throw new Error(`Invalid toad = ${_toad} for wit = ${wits}`); + } + } else { + if (_toad != 0) { + throw new Error(`Invalid toad = ${_toad} for wit = ${wits}`); + } + } + const _sad = { + v: vs, + t: _ilk, + d: '', + i: pre, + s: sner.numh, + p: dig, + kt: + tholder.num && + intive && + tholder.num !== undefined && + tholder.num <= MaxIntThold + ? tholder.num.toString(16) + : tholder.sith, + k: keys, + nt: + ntholder.num && + intive && + ntholder.num !== undefined && + ntholder.num <= MaxIntThold + ? ntholder.num.toString(16) + : ntholder.sith, + n: _ndigs, + bt: + _toad && intive && _toad !== undefined && _toad <= MaxIntThold + ? _toad + : _toad.toString(16), + br: cuts, + ba: adds, + a: data != undefined ? data : [], + }; + const [, sad] = Saider.saidify(_sad); + return new Serder(sad); +} + +export function ample(n: number, f?: number, weak = true) { + n = Math.max(0, n); // no negatives + let f1; + if (f == undefined) { + f1 = Math.max(1, Math.floor(Math.max(0, n - 1) / 3)); // least floor f subject to n >= 3*f+1 + + const f2 = Math.max(1, Math.ceil(Math.max(0, n - 1) / 3)); // most Math.ceil f subject to n >= 3*f+1 + if (weak) { + // try both fs to see which one has lowest m + return Math.min( + n, + Math.ceil((n + f1 + 1) / 2), + Math.ceil((n + f2 + 1) / 2) + ); + } else { + return Math.min( + n, + Math.max(0, n - f1, Math.ceil((n + f1 + 1) / 2)) + ); + } + } else { + f = Math.max(0, f); + const m1 = Math.ceil((n + f + 1) / 2); + const m2 = Math.max(0, n - f); + if (m2 < m1 && n > 0) { + throw new Error(`Invalid f=${f} is too big for n=${n}.`); + } + if (weak) { + return Math.min(n, m1, m2); + } else { + return Math.min(n, Math.max(m1, m2)); + } + } +} + +export interface InceptArgs { + keys: Array; + isith?: number | string | Array; + ndigs?: Array; + nsith?: number | string | Array; + toad?: number | string; + wits?: Array; + cnfg?: Array; + data?: Array; + version?: Version; + kind?: Serials; + code?: string; + intive?: boolean; + delpre?: string; +} + +export function incept({ + keys, + isith, + ndigs, + nsith, + toad, + wits, + cnfg, + data, + version = Vrsn_1_0, + kind = Serials.JSON, + code, + intive = false, + delpre, +}: InceptArgs) { + const vs = versify(Protocols.KERI, version, kind, 0); + const ilk = delpre == undefined ? Ilks.icp : Ilks.dip; + const sner = new CesrNumber({}, 0); + + if (isith == undefined) { + isith = Math.max(1, Math.ceil(keys.length / 2)); + } + + const tholder = new Tholder({ sith: isith }); + if (tholder.num != undefined && tholder.num < 1) { + throw new Error(`Invalid sith = ${tholder.num} less than 1.`); + } + if (tholder.size > keys.length) { + throw new Error(`Invalid sith = ${tholder.num} for keys ${keys}`); + } + + if (ndigs == undefined) { + ndigs = new Array(); + } + + if (nsith == undefined) { + nsith = Math.max(0, Math.ceil(ndigs.length / 2)); + } + + const ntholder = new Tholder({ sith: nsith }); + if (ntholder.num != undefined && ntholder.num < 0) { + throw new Error(`Invalid nsith = ${ntholder.num} less than 0.`); + } + if (ntholder.size > keys.length) { + throw new Error(`Invalid nsith = ${ntholder.num} for keys ${ndigs}`); + } + + wits = wits == undefined ? [] : wits; + if (new Set(wits).size != wits.length) { + throw new Error(`Invalid wits = ${wits}, has duplicates.`); + } + + if (toad == undefined) { + if (wits.length == 0) { + toad = 0; + } else { + toad = ample(wits.length); + } + } + + const toader = new CesrNumber({}, toad); + if (wits.length > 0) { + if (toader.num < 1 || toader.num > wits.length) { + throw new Error(`Invalid toad = ${toader.num} for wits = ${wits}`); + } + } else { + if (toader.num != 0) { + throw new Error(`Invalid toad = ${toader.num} for wits = ${wits}`); + } + } + + cnfg = cnfg == undefined ? new Array() : cnfg; + data = data == undefined ? new Array() : data; + + let sad = { + v: vs, + t: ilk, + d: '', + i: '', + s: sner.numh, + kt: intive && tholder.num != undefined ? tholder.num : tholder.sith, + k: keys, + nt: intive && tholder.num != undefined ? ntholder.num : ntholder.sith, + n: ndigs, + bt: intive ? toader.num : toader.numh, + b: wits, + c: cnfg, + a: data, + } as Dict; + + if (delpre != undefined) { + sad['di'] = delpre; + if (code == undefined) { + code = MtrDex.Blake3_256; + } + } + + let prefixer; + if (delpre == undefined && code == undefined && keys.length == 1) { + prefixer = new Prefixer({ qb64: keys[0] }); + if (prefixer.digestive) { + throw new Error( + `Invalid code, digestive=${prefixer.code}, must be derived from ked.` + ); + } + } else { + prefixer = new Prefixer({ code: code }, sad); + if (delpre != undefined) { + if (!prefixer.digestive) { + throw new Error( + `Invalid derivation code = ${prefixer.code} for delegation. Must be digestive` + ); + } + } + } + + sad['i'] = prefixer.qb64; + if (prefixer.digestive) { + sad['d'] = prefixer.qb64; + } else { + [, sad] = Saider.saidify(sad); + } + + return new Serder(sad); +} + +export function messagize( + serder: Serder, + sigers?: Array, + seal?: any, + wigers?: Array, + cigars?: Array, + pipelined: boolean = false +): Uint8Array { + let msg: Uint8Array = new Uint8Array(b(serder.raw)); + let atc: Uint8Array = new Uint8Array(); + + if (sigers == undefined && wigers == undefined && cigars == undefined) { + throw new Error( + `Missing attached signatures on message = ${serder.sad}.` + ); + } + + if (sigers != undefined) { + if (seal != undefined) { + if (seal[0] == 'SealEvent') { + atc = concat( + atc, + new Counter({ code: CtrDex.TransIdxSigGroups, count: 1 }) + .qb64b + ); + atc = concat(atc, new TextEncoder().encode(seal[1].i)); + atc = concat( + atc, + new Seqner({ sn: parseInt(seal[1].s) }).qb64b + ); + atc = concat(atc, new TextEncoder().encode(seal[1].d)); + } else if (seal[0] == 'SealLast') { + atc = concat( + atc, + new Counter({ + code: CtrDex.TransLastIdxSigGroups, + count: 1, + }).qb64b + ); + atc = concat(atc, new TextEncoder().encode(seal[1].i)); + } + } + + atc = concat( + atc, + new Counter({ + code: CtrDex.ControllerIdxSigs, + count: sigers.length, + }).qb64b + ); + sigers.forEach((siger) => { + atc = concat(atc, siger.qb64b); + }); + } + + if (wigers != undefined) { + atc = concat( + atc, + new Counter({ + code: CtrDex.ControllerIdxSigs, + count: wigers.length, + }).qb64b + ); + + wigers.forEach((wiger) => { + if (wiger.verfer && !(wiger.verfer.code in NonTransDex)) { + throw new Error( + `Attempt to use tranferable prefix=${wiger.verfer.qb64} for receipt.` + ); + } + atc = concat(atc, wiger.qb64b); + }); + } + + if (cigars != undefined) { + atc = concat( + atc, + new Counter({ + code: CtrDex.ControllerIdxSigs, + count: cigars.length, + }).qb64b + ); + + cigars.forEach((cigar) => { + if (cigar.verfer && !(cigar.verfer.code in NonTransDex)) { + throw new Error( + `Attempt to use tranferable prefix=${cigar.verfer.qb64} for receipt.` + ); + } + atc = concat(atc, cigar.qb64b); + }); + } + + if (pipelined) { + if (atc.length % 4 != 0) { + throw new Error( + `Invalid attachments size=${atc.length}, nonintegral quadlets.` + ); + } + msg = concat( + msg, + new Counter({ + code: CtrDex.AttachedMaterialQuadlets, + count: Math.floor(atc.length / 4), + }).qb64b + ); + } + msg = concat(msg, atc); + return msg; +} + +interface InteractArgs { + pre: string; + dig: string; + sn: number; + data: Array; + version: Version | undefined; + kind: Serials | undefined; +} + +export function interact(args: InteractArgs): Serder { + let { pre, dig, sn, data, version, kind } = args; + const vs = versify(Protocols.KERI, version, kind, 0); + const ilk = Ilks.ixn; + const sner = new CesrNumber({}, sn); + + if (sner.num < 1) { + throw new Error(`Invalid sn = 0x${sner.numh} for ixn.`); + } + + data = data == undefined ? new Array() : data; + + let sad = { + v: vs, + t: ilk, + d: '', + i: pre, + s: sner.numh, + p: dig, + a: data, + } as Dict; + + [, sad] = Saider.saidify(sad); + + return new Serder(sad); +} + +export function reply( + route: string = '', + data: any | undefined, + stamp: string | undefined, + version: Version | undefined, + kind: Serials = Serials.JSON +) { + const vs = versify(Protocols.KERI, version, kind, 0); + if (data == undefined) { + data = {}; + } + const _sad = { + v: vs, + t: Ilks.rpy, + d: '', + dt: stamp ?? new Date().toISOString().replace('Z', '000+00:00'), + r: route, + a: data, + }; + const [, sad] = Saider.saidify(_sad); + const saider = new Saider({ qb64: sad['d'] }); + + if (!saider.verify(sad, true, true, kind, 'd')) + throw new Error(`Invalid said = ${saider.qb64} for reply msg=${sad}.`); + return new Serder(sad); +} diff --git a/packages/signify-ts/src/keri/core/httping.ts b/packages/signify-ts/src/keri/core/httping.ts new file mode 100644 index 00000000..b0a881c5 --- /dev/null +++ b/packages/signify-ts/src/keri/core/httping.ts @@ -0,0 +1,217 @@ +import { + serializeDictionary, + Dictionary, + parseDictionary, + Item, + Parameters, +} from 'structured-headers'; +import { Signer } from './signer.ts'; +import { b } from './core.ts'; +import { Cigar } from './cigar.ts'; +import { nowUTC } from './utils.ts'; +import { Siger } from './siger.ts'; +import { encodeBase64Url } from './base64.ts'; + +export const HEADER_SIG_INPUT = normalize('Signature-Input'); +export const HEADER_SIG_TIME = normalize('Signify-Timestamp'); + +export function normalize(header: string) { + return header.trim(); +} + +export interface SiginputArgs { + name: string; + method: string; + path: string; + headers: Headers; + fields: Array; + expires?: number; + nonce?: string; + alg?: string; + keyid?: string; + context?: string; +} + +/** + * Generates, serializes, and signs a Signature-Input HTTP header value as a structured header + * @param signer + * @param sigInputArgs + */ +export function siginput( + signer: Signer, + sigInputArgs: SiginputArgs +): [Map, Siger | Cigar] { + const { + name, + method, + path, + headers, + fields, + expires, + nonce, + alg, + keyid, + context, + } = sigInputArgs; + const items = new Array(); + const ifields = new Array<[string, Map]>(); + + fields.forEach((field) => { + if (field.startsWith('@')) { + switch (field) { + case '@method': + items.push(`"${field}": ${method}`); + ifields.push([field, new Map()]); + break; + case '@path': + items.push(`"${field}": ${path}`); + ifields.push([field, new Map()]); + break; + } + } else { + if (!headers.has(field)) return; + + ifields.push([field, new Map()]); + const value = normalize(headers.get(field)!); + items.push(`"${field}": ${value}`); + } + }); + + const nameParams = new Map(); + const now = Math.floor(nowUTC().getTime() / 1000); + nameParams.set('created', now); + + const values = [ + `(${ifields.map((field) => field[0]).join(' ')})`, + `created=${now}`, + ]; + if (expires != undefined) { + values.push(`expires=${expires}`); + nameParams.set('expires', expires); + } + if (nonce != undefined) { + values.push(`nonce=${nonce}`); + nameParams.set('nonce', nonce); + } + if (keyid != undefined) { + values.push(`keyid=${keyid}`); + nameParams.set('keyid', keyid); + } + if (context != undefined) { + values.push(`context=${context}`); + nameParams.set('context', context); + } + if (alg != undefined) { + values.push(`alg=${alg}`); + nameParams.set('alg', alg); + } + const sid = new Map([[name, [ifields, nameParams]]]); + + const params = values.join(';'); + items.push(`"@signature-params: ${params}"`); + + const ser = items.join('\n'); + const sig = signer.sign(b(ser)); + + return [ + new Map([ + [HEADER_SIG_INPUT, `${serializeDictionary(sid as Dictionary)}`], + ]), + sig, + ]; +} + +export class Unqualified { + private readonly _raw: Uint8Array; + + constructor(raw: Uint8Array) { + this._raw = raw; + } + + get qb64(): string { + return encodeBase64Url(this._raw); + } + + get qb64b(): Uint8Array { + return b(this.qb64); + } +} + +export class Inputage { + public name: any; + public fields: any; + public created: any; + public expires: any; + public nonce: any; + public alg: any; + public keyid: any; + public context: any; +} + +export function desiginput(value: string): Array { + const sid = parseDictionary(value); + const siginputs = new Array(); + + sid.forEach((value, key) => { + const siginput = new Inputage(); + siginput.name = key; + let list: Item[]; + let params; + [list, params] = value as [Item[], Parameters]; + siginput.fields = list.map((item) => item[0]); + + if (!params.has('created')) { + throw new Error( + 'missing required `created` field from signature input' + ); + } + siginput.created = params.get('created'); + + if (params.has('expires')) { + siginput.expires = params.get('expires'); + } + + if (params.has('nonce')) { + siginput.nonce = params.get('nonce'); + } + + if (params.has('alg')) { + siginput.alg = params.get('alg'); + } + + if (params.has('keyid')) { + siginput.keyid = params.get('keyid'); + } + + if (params.has('context')) { + siginput.context = params.get('context'); + } + + siginputs.push(siginput); + }); + + return siginputs; +} +/** Parse start, end and total from HTTP Content-Range header value + * @param {string|null} header - HTTP Range header value + * @param {string} typ - type of range, e.g. "aids" + * @returns {start: number, end: number, total: number} - object with start, end and total properties + */ +export function parseRangeHeaders( + header: string | null, + typ: string +): { start: number; end: number; total: number } { + if (header !== null) { + const data = header.replace(`${typ} `, ''); + const values = data.split('/'); + const rng = values[0].split('-'); + + return { + start: parseInt(rng[0]), + end: parseInt(rng[1]), + total: parseInt(values[1]), + }; + } else { + return { start: 0, end: 0, total: 0 }; + } +} diff --git a/packages/signify-ts/src/keri/core/indexer.ts b/packages/signify-ts/src/keri/core/indexer.ts new file mode 100644 index 00000000..17e86059 --- /dev/null +++ b/packages/signify-ts/src/keri/core/indexer.ts @@ -0,0 +1,507 @@ +import { EmptyMaterialError } from './kering.ts'; +import { b, b64ToInt, d, intToB64, readInt } from './core.ts'; +import { decodeBase64Url, encodeBase64Url } from './base64.ts'; + +export class IndexerCodex { + Ed25519_Sig: string = 'A'; // Ed25519 sig appears same in both lists if any. + Ed25519_Crt_Sig: string = 'B'; // Ed25519 sig appears in current list only. + ECDSA_256k1_Sig: string = 'C'; // ECDSA secp256k1 sig appears same in both lists if any. + ECDSA_256k1_Crt_Sig: string = 'D'; // ECDSA secp256k1 sig appears in current list. + ECDSA_256r1_Sig: string = 'E'; // ECDSA secp256r1 sig appears same in both lists if any. + ECDSA_256r1_Crt_Sig: string = 'F'; // ECDSA secp256r1 sig appears in current list. + Ed448_Sig: string = '0A'; // Ed448 signature appears in both lists. + Ed448_Crt_Sig: string = '0B'; // Ed448 signature appears in current list only. + Ed25519_Big_Sig: string = '2A'; // Ed25519 sig appears in both lists. + Ed25519_Big_Crt_Sig: string = '2B'; // Ed25519 sig appears in current list only. + ECDSA_256k1_Big_Sig: string = '2C'; // ECDSA secp256k1 sig appears in both lists. + ECDSA_256k1_Big_Crt_Sig: string = '2D'; // ECDSA secp256k1 sig appears in current list only. + ECDSA_256r1_Big_Sig: string = '2E'; // ECDSA secp256r1 sig appears in both lists. + ECDSA_256r1_Big_Crt_Sig: string = '2F'; // ECDSA secp256r1 sig appears in current list only. + Ed448_Big_Sig: string = '3A'; // Ed448 signature appears in both lists. + Ed448_Big_Crt_Sig: string = '3B'; // Ed448 signature appears in current list only. +} + +export const IdrDex = new IndexerCodex(); + +export class IndexedSigCodex { + Ed25519_Sig: string = 'A'; // Ed25519 sig appears same in both lists if any. + Ed25519_Crt_Sig: string = 'B'; // Ed25519 sig appears in current list only. + ECDSA_256k1_Sig: string = 'C'; // ECDSA secp256k1 sig appears same in both lists if any. + ECDSA_256k1_Crt_Sig: string = 'D'; // ECDSA secp256k1 sig appears in current list. + ECDSA_256r1_Sig: string = 'E'; // ECDSA secp256r1 sig appears same in both lists if any. + ECDSA_256r1_Crt_Sig: string = 'F'; // ECDSA secp256r1 sig appears in current list. + Ed448_Sig: string = '0A'; // Ed448 signature appears in both lists. + Ed448_Crt_Sig: string = '0B'; // Ed448 signature appears in current list only. + Ed25519_Big_Sig: string = '2A'; // Ed25519 sig appears in both lists. + Ed25519_Big_Crt_Sig: string = '2B'; // Ed25519 sig appears in current list only. + ECDSA_256k1_Big_Sig: string = '2C'; // ECDSA secp256k1 sig appears in both lists. + ECDSA_256k1_Big_Crt_Sig: string = '2D'; // ECDSA secp256k1 sig appears in current list only. + ECDSA_256r1_Big_Sig: string = '2E'; // ECDSA secp256r1 sig appears in both lists. + ECDSA_256r1_Big_Crt_Sig: string = '2F'; // ECDSA secp256r1 sig appears in current list only. + Ed448_Big_Sig: string = '3A'; // Ed448 signature appears in both lists. + Ed448_Big_Crt_Sig: string = '3B'; // Ed448 signature appears in current list only. + + has(prop: string): boolean { + const m = new Map( + Array.from(Object.entries(this), (v) => [v[1], v[0]]) + ); + return m.has(prop); + } +} + +export const IdxSigDex = new IndexedSigCodex(); + +export class IndexedCurrentSigCodex { + Ed25519_Crt_Sig: string = 'B'; // Ed25519 sig appears in current list only. + ECDSA_256k1_Crt_Sig: string = 'D'; // ECDSA secp256k1 sig appears in current list only. + ECDSA_256r1_Crt_Sig: string = 'F'; // ECDSA secp256r1 sig appears in current list. + Ed448_Crt_Sig: string = '0B'; // Ed448 signature appears in current list only. + Ed25519_Big_Crt_Sig: string = '2B'; // Ed25519 sig appears in current list only. + ECDSA_256k1_Big_Crt_Sig: string = '2D'; // ECDSA secp256k1 sig appears in current list only. + ECDSA_256r1_Big_Crt_Sig: string = '2F'; // ECDSA secp256r1 sig appears in current list only. + Ed448_Big_Crt_Sig: string = '3B'; // Ed448 signature appears in current list only. + + has(prop: string): boolean { + const m = new Map( + Array.from(Object.entries(this), (v) => [v[1], v[0]]) + ); + return m.has(prop); + } +} + +export const IdxCrtSigDex = new IndexedCurrentSigCodex(); + +export class IndexedBothSigCodex { + Ed25519_Sig: string = 'A'; // Ed25519 sig appears same in both lists if any. + ECDSA_256k1_Sig: string = 'C'; // ECDSA secp256k1 sig appears same in both lists if any. + Ed448_Sig: string = '0A'; // Ed448 signature appears in both lists. + Ed25519_Big_Sig: string = '2A'; // Ed25519 sig appears in both listsy. + ECDSA_256k1_Big_Sig: string = '2C'; // ECDSA secp256k1 sig appears in both lists. + Ed448_Big_Sig: string = '3A'; // Ed448 signature appears in both lists. + + has(prop: string): boolean { + const m = new Map( + Array.from(Object.entries(this), (v) => [v[1], v[0]]) + ); + return m.has(prop); + } +} + +export const IdxBthSigDex = new IndexedBothSigCodex(); + +export class Xizage { + public hs: number; + public ss: number; + public os: number; + public fs?: number; + public ls: number; + + constructor(hs: number, ss: number, os: number, fs?: number, ls?: number) { + this.hs = hs; + this.ss = ss; + this.os = os; + this.fs = fs; + this.ls = ls!; + } +} + +export interface IndexerArgs { + raw?: Uint8Array | undefined; + code?: string | undefined; + index?: number; + ondex?: number; + qb64b?: Uint8Array | undefined; + qb64?: string | undefined; + qb2?: Uint8Array | undefined; +} + +export class Indexer { + public Codex = IdrDex; + + static Hards = new Map([ + ['A', 1], + ['B', 1], + ['C', 1], + ['D', 1], + ['E', 1], + ['F', 1], + ['G', 1], + ['H', 1], + ['I', 1], + ['J', 1], + ['K', 1], + ['L', 1], + ['M', 1], + ['N', 1], + ['O', 1], + ['P', 1], + ['Q', 1], + ['R', 1], + ['S', 1], + ['T', 1], + ['U', 1], + ['V', 1], + ['W', 1], + ['X', 1], + ['Y', 1], + ['Z', 1], + ['a', 1], + ['b', 1], + ['c', 1], + ['d', 1], + ['e', 1], + ['f', 1], + ['g', 1], + ['h', 1], + ['i', 1], + ['j', 1], + ['k', 1], + ['l', 1], + ['m', 1], + ['n', 1], + ['o', 1], + ['p', 1], + ['q', 1], + ['r', 1], + ['s', 1], + ['t', 1], + ['u', 1], + ['v', 1], + ['w', 1], + ['x', 1], + ['y', 1], + ['z', 1], + ['0', 2], + ['1', 2], + ['2', 2], + ['3', 2], + ['4', 2], + ]); + + static Sizes = new Map( + Object.entries({ + A: new Xizage(1, 1, 0, 88, 0), + B: new Xizage(1, 1, 0, 88, 0), + C: new Xizage(1, 1, 0, 88, 0), + D: new Xizage(1, 1, 0, 88, 0), + E: new Xizage(1, 1, 0, 88, 0), + F: new Xizage(1, 1, 0, 88, 0), + '0A': new Xizage(2, 2, 1, 156, 0), + '0B': new Xizage(2, 2, 1, 156, 0), + + '2A': new Xizage(2, 4, 2, 92, 0), + '2B': new Xizage(2, 4, 2, 92, 0), + '2C': new Xizage(2, 4, 2, 92, 0), + '2D': new Xizage(2, 4, 2, 92, 0), + '2E': new Xizage(2, 4, 2, 92, 0), + '2F': new Xizage(2, 4, 2, 92, 0), + + '3A': new Xizage(2, 6, 3, 160, 0), + '3B': new Xizage(2, 6, 3, 160, 0), + + '0z': new Xizage(2, 2, 0, undefined, 0), + '1z': new Xizage(2, 2, 1, 76, 1), + '4z': new Xizage(2, 6, 3, 80, 1), + }) + ); + + private _code: string = ''; + private _index: number = -1; + private _ondex: number | undefined; + private _raw: Uint8Array = new Uint8Array(0); + + constructor({ + raw = undefined, + code = IdrDex.Ed25519_Sig, + index = 0, + ondex = undefined, + qb64b = undefined, + qb64 = undefined, + qb2 = undefined, + }: IndexerArgs) { + if (raw != undefined) { + if (code == undefined) { + throw new EmptyMaterialError( + `Improper initialization need either (raw and code) or qb64b or qb64 or qb2.` + ); + } + + if (!Indexer.Sizes.has(code)) { + throw new Error(`Unsupported code=${code}.`); + } + + const xizage = Indexer.Sizes.get(code)!; + const os = xizage.os; + const fs = xizage.fs; + const cs = xizage.hs + xizage.ss; + const ms = xizage.ss - xizage.os; + + if (!Number.isInteger(index) || index < 0 || index > 64 ** ms - 1) { + throw new Error(`Invalid index=${index} for code=${code}.`); + } + + if ( + ondex != undefined && + xizage.os != 0 && + !(ondex >= 0 && ondex <= 64 ** os - 1) + ) { + throw new Error(`Invalid ondex=${ondex} for code=${code}.`); + } + + if (IdxCrtSigDex.has(code) && ondex != undefined) { + throw new Error(`Non None ondex=${ondex} for code=${code}.`); + } + + if (IdxBthSigDex.has(code)) { + if (ondex == undefined) { + ondex = index; + } else { + if (ondex != index && os == 0) { + throw new Error( + `Non matching ondex=${ondex} and index=${index} for code=${code}.` + ); + } + } + } + + if (fs == undefined) { + throw new Error('variable length unsupported'); + } + // TODO: Don't support this code + // if not fs: # compute fs from index + // if cs % 4: + // raise InvalidCodeSizeError(f"Whole code size not multiple of 4 for " + // f"variable length material. cs={cs}.") + // if os != 0: + // raise InvalidCodeSizeError(f"Non-zero other index size for " + // f"variable length material. os={os}.") + // fs = (index * 4) + cs + const rawsize = Math.floor(((fs - cs) * 3) / 4); + raw = raw.slice(0, rawsize); + + if (raw.length != rawsize) { + throw new Error( + `Not enougth raw bytes for code=${code} and index=${index} ,expected ${rawsize} got ${raw.length}.` + ); + } + + this._code = code; + this._index = index; + this._ondex = ondex; + this._raw = raw; + } else if (qb64b != undefined) { + const qb64 = d(qb64b); + this._exfil(qb64); + } else if (qb64 != undefined) { + this._exfil(qb64); + } else if (qb2 != undefined) { + this._bexfil(qb2); + } else { + throw new EmptyMaterialError( + `Improper initialization need either (raw and code and index) or qb64b or qb64 or qb2.` + ); + } + } + + private _bexfil(qb2: Uint8Array) { + throw new Error(`qb2 not yet supported: ${qb2}`); + } + + public static _rawSize(code: string) { + const xizage = Indexer.Sizes.get(code)!; + return Math.floor(xizage.fs! - ((xizage.hs + xizage.ss) * 3) / 4); + } + + get code(): string { + return this._code; + } + + get raw(): Uint8Array { + return this._raw; + } + + get index(): number { + return this._index; + } + + get ondex(): number | undefined { + return this._ondex; + } + + get qb64(): string { + return this._infil(); + } + + get qb64b() { + return b(this.qb64); + } + + private _infil(): string { + const code = this.code; + const index = this.index; + const ondex = this.ondex; + const raw = this.raw; + + const ps = (3 - (raw.length % 3)) % 3; + const xizage = Indexer.Sizes.get(code)!; + const cs = xizage.hs + xizage.ss; + const ms = xizage.ss - xizage.os; + + // TODO: don't support this code + // if not fs: # compute fs from index + // if cs % 4: + // raise InvalidCodeSizeError(f"Whole code size not multiple of 4 for " + // f"variable length material. cs={cs}.") + // if os != 0: + // raise InvalidCodeSizeError(f"Non-zero other index size for " + // f"variable length material. os={os}.") + // fs = (index * 4) + cs + + if (index < 0 || index > 64 ** ms - 1) { + throw new Error(`Invalid index=${index} for code=${code}.`); + } + + if ( + ondex != undefined && + xizage.os != 0 && + !(ondex >= 0 && ondex <= 64 ** xizage.os - 1) + ) { + throw new Error( + `Invalid ondex=${ondex} for os=${xizage.os} and code=${code}.` + ); + } + + const both = `${code}${intToB64(index, ms)}${intToB64( + ondex == undefined ? 0 : ondex, + xizage.os + )}`; + + if (both.length != cs) { + throw new Error( + `Mismatch code size = ${cs} with table = ${both.length}.` + ); + } + + if (cs % 4 != ps - xizage.ls) { + throw new Error( + `Invalid code=${both} for converted raw pad size=${ps}.` + ); + } + + const bytes = new Uint8Array(ps + raw.length); + for (let i = 0; i < ps; i++) { + bytes[i] = 0; + } + for (let i = 0; i < raw.length; i++) { + const odx = i + ps; + bytes[odx] = raw[i]; + } + + const full = both + encodeBase64Url(bytes).slice(ps - xizage.ls); + if (full.length != xizage.fs) { + throw new Error(`Invalid code=${both} for raw size=${raw.length}.`); + } + + return full; + } + + _exfil(qb64: string) { + if (qb64.length == 0) { + throw new Error('Empty Material'); + } + + const first = qb64[0]; + if (!Array.from(Indexer.Hards.keys()).includes(first)) { + throw new Error(`Unexpected code ${first}`); + } + + const hs = Indexer.Hards.get(first)!; + if (qb64.length < hs) { + throw new Error(`Need ${hs - qb64.length} more characters.`); + } + + const hard = qb64.slice(0, hs); + if (!Array.from(Indexer.Sizes.keys()).includes(hard)) { + throw new Error(`Unsupported code ${hard}`); + } + + const xizage = Indexer.Sizes.get(hard)!; + const cs = xizage.hs + xizage.ss; // both hard + soft code size + const ms = xizage.ss - xizage.os; + + if (qb64.length < cs) { + throw new Error(`Need ${cs - qb64.length} more characters.`); + } + + const sindex = qb64.slice(hs, hs + ms); + const index = b64ToInt(sindex); + + const sondex = qb64.slice(hs + ms, hs + ms + xizage.os); + let ondex; + if (IdxCrtSigDex.has(hard)) { + ondex = xizage.os != 0 ? b64ToInt(sondex) : undefined; + if (ondex != 0 && ondex != undefined) { + throw new Error(`Invalid ondex=${ondex} for code=${hard}.`); + } else { + ondex = undefined; + } + } else { + ondex = xizage.os != 0 ? b64ToInt(sondex) : index; + } + + if (xizage.fs == undefined) { + throw new Error('variable length not supported'); + } + // TODO: support variable length + // if not fs: # compute fs from index which means variable length + // if cs % 4: + // raise ValidationError(f"Whole code size not multiple of 4 for " + // f"variable length material. cs={cs}.") + // if os != 0: + // raise ValidationError(f"Non-zero other index size for " + // f"variable length material. os={os}.") + // fs = (index * 4) + cs + + if (qb64.length < xizage.fs) { + throw new Error(`Need ${xizage.fs - qb64.length} more chars.`); + } + + qb64 = qb64.slice(0, xizage.fs); + const ps = cs % 4; + const pbs = 2 * ps != 0 ? ps : xizage.ls; + let raw; + if (ps != 0) { + const base = new Array(ps + 1).join('A') + qb64.slice(cs); + const paw = decodeBase64Url(base); // decode base to leave prepadded raw + const pi = readInt(paw.slice(0, ps)); // prepad as int + if (pi & (2 ** pbs - 1)) { + // masked pad bits non-zero + throw new Error( + `Non zeroed prepad bits = {pi & (2 ** pbs - 1 ):<06b} in {qb64b[cs:cs+1]}.` + ); + } + raw = paw.slice(ps); // strip off ps prepad paw bytes + } else { + const base = qb64.slice(cs); + const paw = decodeBase64Url(base); + const li = readInt(paw.slice(0, xizage!.ls)); + if (li != 0) { + if (li == 1) { + throw new Error(`Non zeroed lead byte = 0x{li:02x}.`); + } else { + throw new Error(`Non zeroed lead bytes = 0x{li:04x}`); + } + } + raw = paw.slice(xizage!.ls); + } + + if (raw.length != Math.floor(((qb64.length - cs) * 3) / 4)) { + throw new Error(`Improperly qualified material = ${qb64}`); + } + + this._code = hard; + this._index = index; + this._ondex = ondex; + this._raw = new Uint8Array(raw); // must be bytes for crpto opts and immutable not bytearray + } +} diff --git a/packages/signify-ts/src/keri/core/keeping.ts b/packages/signify-ts/src/keri/core/keeping.ts new file mode 100644 index 00000000..92651323 --- /dev/null +++ b/packages/signify-ts/src/keri/core/keeping.ts @@ -0,0 +1,759 @@ +import { Salter } from './salter.ts'; +import { Algos, SaltyCreator, RandyCreator } from './manager.ts'; +import { MtrDex } from './matter.ts'; +import { Tier } from './salter.ts'; +import { Encrypter } from '../core/encrypter.ts'; +import { Decrypter } from './decrypter.ts'; +import { b } from './core.ts'; +import { Cipher } from './cipher.ts'; +import { Diger } from './diger.ts'; +import { Prefixer } from './prefixer.ts'; +import { Signer } from './signer.ts'; +import { + ExternState, + GroupKeyState, + HabState, + RandyKeyState, + SaltyKeyState, + KeyState, +} from './keyState.ts'; + +/** External module definition */ +export interface ExternalModuleType { + new (pidx: number, args: IdentifierManagerParams): IdentifierManager; +} + +export interface ExternalModule { + type: string; + name: string; + module: ExternalModuleType; +} + +export type IdentifierManagerResult = [string[], string[]]; +export type SignResult = string[]; + +export interface IdentifierManagerParams { + [key: string]: unknown; +} + +export interface SaltyManagerParams extends IdentifierManagerParams { + pidx: number; + kidx: number; + tier: Tier; + transferable: boolean; + stem: string | undefined; + icodes: string[] | undefined; + ncodes: string[] | undefined; + dcode: string | undefined; + sxlt: string | undefined; +} + +export interface RandyManagerParams extends IdentifierManagerParams { + nxts?: string[]; + prxs?: string[]; + transferable: boolean; +} + +export interface GroupManagerParams extends IdentifierManagerParams { + mhab: HabState; +} + +/** + * Interface for KERI identifier (prefix) creation, rotation, and signing. + * @param T Type of the key keeper + */ +export interface IdentifierManager< + T extends IdentifierManagerParams = IdentifierManagerParams, +> { + algo: Algos; + signers: Signer[]; + params(): T; + incept(transferable: boolean): Promise; + rotate( + ncodes: string[], + transferable: boolean, + states?: KeyState[], + rstates?: KeyState[] + ): Promise; + sign( + ser: Uint8Array, + indexed?: boolean, + indices?: number[], + ondices?: number[] + ): Promise; +} + +/** + * Creates IdentifierManager instances based on the algorithm and key indexes. + */ +export class IdentifierManagerFactory { + private modules: Record = {}; + + /** + * Creates a factory for generating IdentifierManagers. Requires a salt to be specified. + * Allows external key management modules to be configured. + * @param salter + * @param externalModules + */ + constructor( + private salter: Salter, + externalModules: ExternalModule[] = [] + ) { + this.salter = salter; + + for (const mod of externalModules) { + this.modules[mod.type] = mod.module; + } + } + + /** + * + * @param algo + * @param pidx + * @param kargs + */ + new(algo: Algos, pidx: number, kargs: any) { + switch (algo) { + case Algos.salty: + return new SaltyIdentifierManager( + this.salter!, + pidx, + kargs['kidx'], + kargs['tier'], + kargs['transferable'], + kargs['stem'], + kargs['code'], + kargs['count'], + kargs['icodes'], + kargs['ncode'], + kargs['ncount'], + kargs['ncodes'], + kargs['dcode'], + kargs['bran'], + kargs['sxlt'] + ); + case Algos.randy: + return new RandyIdentifierManager( + this.salter!, + kargs['code'], + kargs['count'], + kargs['icodes'], + kargs['transferable'], + kargs['ncode'], + kargs['ncount'], + kargs['ncodes'], + kargs['dcode'], + kargs['prxs'], + kargs['nxts'] + ); + case Algos.group: + return new GroupIdentifierManager( + this, + kargs['mhab'], + kargs['states'], + kargs['rstates'], + kargs['keys'], + kargs['ndigs'] + ); + case Algos.extern: { + const ModuleConstructor = this.modules[kargs.extern_type]; + if (!ModuleConstructor) { + throw new Error( + `unsupported external module type ${kargs.extern_type}` + ); + } + + return new ModuleConstructor(pidx, kargs); + } + default: + throw new Error('Unknown algo'); + } + } + + /** + * Generates an algorithm-specific IdentifierManager instance with correct keys based on + * the indexes provided by the HabState. + * @param aid HabState with the algorithm and key indexes + * @returns IdentifierManager instance + */ + get(aid: HabState): IdentifierManager { + if (Algos.salty in aid) { + return new SaltyIdentifierManager( + this.salter, + aid.salty.pidx, + aid.salty.kidx, + aid.salty.tier, + aid.salty.transferable, + aid.salty.stem, + undefined, + undefined, + aid.salty.icodes, + undefined, + undefined, + aid.salty.ncodes, + aid.salty.dcode, + undefined, + aid.salty.sxlt + ); + } else if (Algos.randy in aid) { + return new RandyIdentifierManager( + this.salter, + undefined, + undefined, + undefined, + new Prefixer({ qb64: aid['prefix'] }).transferable, + undefined, + undefined, + [], + undefined, + aid.randy.prxs, + aid.randy.nxts + ); + } else if (Algos.group in aid) { + return new GroupIdentifierManager( + this, + aid.group.mhab, + undefined, + undefined, + aid.group.keys, + aid.group.ndigs + ); + } else if (Algos.extern in aid) { + const typ = aid.extern.extern_type; + if (typ in this.modules) { + const mod = new this.modules[typ](aid.extern.pidx, aid.extern); + return mod; + } else { + throw new Error(`unsupported external module type ${typ}`); + } + } else { + throw new Error('No algo specified'); + } + } +} + +export class SaltyIdentifierManager implements IdentifierManager { + private aeid: string; + private encrypter: Encrypter; + private decrypter: Decrypter; + private salter: Salter; + private pidx: number; + private kidx: number; + private tier: Tier; + private transferable: boolean; + private stem: string | undefined; + private code: string; + private count: number; + private icodes: string[] | undefined; + private ncode: string; + private ncount: number; + private ncodes: string[] | undefined; + private dcode: string | undefined; + private sxlt: string | undefined; + private bran: string | undefined; + private creator: SaltyCreator; + public algo: Algos = Algos.salty; + public signers: Signer[]; + + constructor( + salter: Salter, + pidx: number, + kidx: number = 0, + tier = Tier.low, + transferable = false, + stem: string | undefined = undefined, + code = MtrDex.Ed25519_Seed, + count = 1, + icodes: string[] | undefined = undefined, + ncode = MtrDex.Ed25519_Seed, + ncount = 1, + ncodes: string[] | undefined = undefined, + dcode = MtrDex.Blake3_256, + bran: string | undefined = undefined, + sxlt: string | undefined = undefined + ) { + // # Salter is the entered passcode and used for enc/dec of salts for each AID + this.salter = salter; + const signer = this.salter.signer(code, transferable, undefined, tier); + + this.aeid = signer.verfer.qb64; + + this.encrypter = new Encrypter({}, b(this.aeid)); + this.decrypter = new Decrypter({}, signer.qb64b); + + this.code = code; + this.ncode = ncode; + this.tier = tier; + this.icodes = + icodes == undefined ? new Array(count).fill(code) : icodes; + this.ncodes = + ncodes == undefined + ? new Array(ncount).fill(ncode) + : ncodes; + this.dcode = dcode; + this.pidx = pidx; + this.kidx = kidx; + this.transferable = transferable; + this.count = count; + this.ncount = ncount; + this.stem = stem == undefined ? 'signify:aid' : stem; + + if (bran != undefined) { + this.bran = MtrDex.Salt_128 + 'A' + bran!.slice(0, 21); + this.creator = new SaltyCreator(this.bran, this.tier, this.stem); + this.sxlt = this.encrypter.encrypt(b(this.creator.salt)).qb64; + } else if (sxlt == undefined) { + this.creator = new SaltyCreator(undefined, this.tier, this.stem); + this.sxlt = this.encrypter.encrypt(b(this.creator.salt)).qb64; + } else { + this.sxlt = sxlt; + const ciph = new Cipher({ qb64: this.sxlt }); + this.creator = new SaltyCreator( + this.decrypter.decrypt(null, ciph).qb64, + tier, + this.stem + ); + } + + this.signers = this.creator.create( + this.icodes, + this.ncount, + this.ncode, + this.transferable, + this.pidx, + 0, + this.kidx, + false + ).signers; + } + + params(): SaltyManagerParams { + return { + sxlt: this.sxlt, + pidx: this.pidx, + kidx: this.kidx, + stem: this.stem, + tier: this.tier, + icodes: this.icodes, + ncodes: this.ncodes, + dcode: this.dcode, + transferable: this.transferable, + }; + } + + async incept(transferable: boolean): Promise { + this.transferable = transferable; + this.kidx = 0; + + const signers = this.creator.create( + this.icodes, + this.count, + this.code, + this.transferable, + this.pidx, + 0, + this.kidx, + false + ); + const verfers = signers.signers.map((signer) => signer.verfer.qb64); + + const nsigners = this.creator.create( + this.ncodes, + this.ncount, + this.ncode, + this.transferable, + this.pidx, + 0, + this.icodes?.length, + false + ); + const digers = nsigners.signers.map( + (nsigner) => + new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64 + ); + + return [verfers, digers]; + } + + async rotate( + ncodes: string[], + transferable: boolean + ): Promise<[string[], string[]]> { + this.ncodes = ncodes; + this.transferable = transferable; + const signers = this.creator.create( + this.ncodes, + this.ncount, + this.ncode, + this.transferable, + this.pidx, + 0, + this.kidx + this.icodes!.length, + false + ); + const verfers = signers.signers.map((signer) => signer.verfer.qb64); + + this.kidx = this.kidx! + this.icodes!.length; + const nsigners = this.creator.create( + this.ncodes, + this.ncount, + this.ncode, + this.transferable, + this.pidx, + 0, + this.kidx + this.icodes!.length, + false + ); + const digers = nsigners.signers.map( + (nsigner) => + new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64 + ); + + return [verfers, digers]; + } + + async sign( + ser: Uint8Array, + indexed = true, + indices: number[] | undefined = undefined, + ondices: number[] | undefined = undefined + ): Promise { + const signers = this.creator.create( + this.icodes, + this.ncount, + this.ncode, + this.transferable, + this.pidx, + 0, + this.kidx, + false + ); + + if (indexed) { + const sigers = []; + let i = 0; + for (const [j, signer] of signers.signers.entries()) { + if (indices != undefined) { + i = indices![j]; + if (typeof i != 'number' || i < 0) { + throw new Error( + `Invalid signing index = ${i}, not whole number.` + ); + } + } else { + i = j; + } + let o = 0; + if (ondices != undefined) { + o = ondices![j]; + if ( + (o == undefined || + (typeof o == 'number' && + typeof o != 'number' && + o >= 0))! + ) { + throw new Error( + `Invalid ondex = ${o}, not whole number.` + ); + } + } else { + o = i; + } + sigers.push( + signer.sign(ser, i, o == undefined ? true : false, o) + ); + } + return sigers.map((siger) => siger.qb64); + } else { + const cigars = []; + for (const [, signer] of signers.signers.entries()) { + cigars.push(signer.sign(ser)); + } + return cigars.map((cigar) => cigar.qb64); + } + } +} + +export class RandyIdentifierManager implements IdentifierManager { + private salter: Salter; + private code: string; + private count: number; + private icodes: string[] | undefined; + private transferable: boolean; + private ncount: number; + private ncodes: string[] | undefined; + private ncode: string; + private dcode: string | undefined; + private prxs: string[] | undefined; + private nxts: string[] | undefined; + private aeid: string; + private encrypter: Encrypter; + private decrypter: Decrypter; + private creator: RandyCreator; + public algo: Algos = Algos.randy; + public signers: Signer[]; + + constructor( + salter: Salter, + code = MtrDex.Ed25519_Seed, + count = 1, + icodes: string[] | undefined = undefined, + transferable = false, + ncode = MtrDex.Ed25519_Seed, + ncount = 1, + ncodes: string[], + dcode = MtrDex.Blake3_256, + prxs: string[] | undefined = undefined, + nxts: string[] | undefined = undefined + ) { + this.salter = salter; + this.icodes = + icodes == undefined ? new Array(count).fill(code) : icodes; + this.ncodes = + ncodes == undefined + ? new Array(ncount).fill(ncode) + : ncodes; + + this.code = code; + this.ncode = ncode; + this.count = count; + this.ncount = ncount; + + const signer = this.salter.signer(code, transferable); + this.aeid = signer.verfer.qb64; + + this.encrypter = new Encrypter({}, b(this.aeid)); + this.decrypter = new Decrypter({}, signer.qb64b); + + this.nxts = nxts ?? []; + this.prxs = prxs ?? []; + this.transferable = transferable; + + this.icodes = icodes; + this.ncodes = ncodes; + this.dcode = dcode; + + this.creator = new RandyCreator(); + + this.signers = this.prxs.map((prx) => + this.decrypter.decrypt( + new Cipher({ qb64: prx }).qb64b, + undefined, + this.transferable + ) + ); + } + + params(): RandyManagerParams { + return { + nxts: this.nxts, + prxs: this.prxs, + transferable: this.transferable, + }; + } + + async incept(transferable: boolean): Promise { + this.transferable = transferable; + + const signers = this.creator.create( + this.icodes, + this.count, + this.code, + this.transferable + ); + this.prxs = signers.signers.map( + (signer) => this.encrypter.encrypt(undefined, signer).qb64 + ); + + const verfers = signers.signers.map((signer) => signer.verfer.qb64); + + const nsigners = this.creator.create( + this.ncodes, + this.ncount, + this.ncode, + this.transferable + ); + + this.nxts = nsigners.signers.map( + (signer) => this.encrypter.encrypt(undefined, signer).qb64 + ); + + const digers = nsigners.signers.map( + (nsigner) => + new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64 + ); + + return [verfers, digers]; + } + + async rotate( + ncodes: string[], + transferable: boolean + ): Promise { + this.ncodes = ncodes; + this.transferable = transferable; + this.prxs = this.nxts; + + const signers = this.nxts!.map((nxt) => + this.decrypter.decrypt( + undefined, + new Cipher({ qb64: nxt }), + this.transferable + ) + ); + const verfers = signers.map((signer) => signer.verfer.qb64); + const nsigners = this.creator.create( + this.ncodes, + this.ncount, + this.ncode, + this.transferable + ); + + this.nxts = nsigners.signers.map( + (signer) => this.encrypter.encrypt(undefined, signer).qb64 + ); + + const digers = nsigners.signers.map( + (nsigner) => + new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64 + ); + + return [verfers, digers]; + } + + async sign( + ser: Uint8Array, + indexed = true, + indices: number[] | undefined = undefined, + ondices: number[] | undefined = undefined + ): Promise { + const signers = this.prxs!.map((prx) => + this.decrypter.decrypt( + new Cipher({ qb64: prx }).qb64b, + undefined, + this.transferable + ) + ); + + if (indexed) { + const sigers = []; + let i = 0; + for (const [j, signer] of signers.entries()) { + if (indices != undefined) { + i = indices![j]; + if (typeof i != 'number' || i < 0) { + throw new Error( + `Invalid signing index = ${i}, not whole number.` + ); + } + } else { + i = j; + } + let o = 0; + if (ondices != undefined) { + o = ondices![j]; + if ( + (o == undefined || + (typeof o == 'number' && + typeof o != 'number' && + o >= 0))! + ) { + throw new Error( + `Invalid ondex = ${o}, not whole number.` + ); + } + } else { + o = i; + } + sigers.push( + signer.sign(ser, i, o == undefined ? true : false, o) + ); + } + return sigers.map((siger) => siger.qb64); + } else { + const cigars = []; + for (const [, signer] of signers.entries()) { + cigars.push(signer.sign(ser)); + } + return cigars.map((cigar) => cigar.qb64); + } + } +} + +export class GroupIdentifierManager implements IdentifierManager { + private manager: IdentifierManagerFactory; + private mhab: HabState; + private gkeys: string[] = []; + private gdigs: string[] = []; + public algo: Algos = Algos.group; + public signers: Signer[]; + + constructor( + manager: IdentifierManagerFactory, + mhab: HabState, + states: KeyState[] | undefined = undefined, + rstates: KeyState[] | undefined = undefined, + keys: string[] = [], + ndigs: string[] = [] + ) { + this.manager = manager; + if (states != undefined) { + keys = states.map((state) => state['k'][0]); + } + + if (rstates != undefined) { + ndigs = rstates.map((state) => state['n'][0]); + } + + this.gkeys = states?.map((state) => state['k'][0]) ?? keys; + this.gdigs = rstates?.map((state) => state['n'][0]) ?? ndigs; + this.mhab = mhab; + this.signers = []; + } + + async incept(): Promise { + return [this.gkeys, this.gdigs]; + } + + /** + * Performs a multisig rotation + * @param _ncodes + * @param _transferable + * @param states + * @param rstates key state records for the prior establishment event indicating next key digests. + * You should pass in the current key + */ + async rotate( + _ncodes: string[], + _transferable: boolean, + states: KeyState[], + rstates: KeyState[] + ): Promise { + this.gkeys = states.map((state) => state['k'][0]); + this.gdigs = rstates.map((state) => state['n'][0]); + return [this.gkeys, this.gdigs]; + } + + async sign(ser: Uint8Array, indexed: boolean = true): Promise { + if (!this.mhab.state) { + throw new Error(`No state in mhab`); + } + + const key = this.mhab['state']['k'][0]; + const ndig = this.mhab['state']['n'][0]; + + const csi = this.gkeys!.indexOf(key); // csi = current signing index (from current rotation event) + const pni = this.gdigs!.indexOf(ndig); // pni = prior next index (from last establishment event) + const mkeeper = this.manager.get(this.mhab); + + return await mkeeper.sign(ser, indexed, [csi], [pni]); + } + + params() { + return { + mhab: this.mhab, + keys: this.gkeys, + ndigs: this.gdigs, + }; + } +} diff --git a/packages/signify-ts/src/keri/core/kering.ts b/packages/signify-ts/src/keri/core/kering.ts new file mode 100644 index 00000000..f0a1a05a --- /dev/null +++ b/packages/signify-ts/src/keri/core/kering.ts @@ -0,0 +1,10 @@ +export class EmptyMaterialError { + private readonly _err: Error; + constructor(err: string) { + this._err = new Error(err); + } + + get err() { + return this._err; + } +} diff --git a/packages/signify-ts/src/keri/core/keyState.ts b/packages/signify-ts/src/keri/core/keyState.ts new file mode 100644 index 00000000..cf26dba4 --- /dev/null +++ b/packages/signify-ts/src/keri/core/keyState.ts @@ -0,0 +1,36 @@ +import { components } from '../../types/keria-api-schema.ts'; + +export type KeyState = components['schemas']['KeyStateRecord']; + +export type EstablishmentState = components['schemas']['StateEERecord']; + +/** + * Marker interface for state configuring an IdentifierManager. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface IdentifierManagerState {} + +/** + * Defining configuration parameters for a specified, deterministic salt of an IdentifierManager. + */ +export type SaltyKeyState = components['schemas']['SaltyState']; + +/** + * Defining configuration parameters for a random seed identifier manager. + */ +export type RandyKeyState = components['schemas']['RandyKeyState']; + +/** + * Defining properties a multi-signature group identifier manager. + */ +export type GroupKeyState = components['schemas']['GroupKeyState']; + +/** + * Defining properties for an external module identifier manager that uses externally managed keys such as in an HSM or a KMS system. + */ +export type ExternState = components['schemas']['ExternState']; + +/** + * Defining properties of an identifier habitat, know as a Hab in KERIpy. + */ +export type HabState = components['schemas']['Identifier']; diff --git a/packages/signify-ts/src/keri/core/manager.ts b/packages/signify-ts/src/keri/core/manager.ts new file mode 100644 index 00000000..59ccd440 --- /dev/null +++ b/packages/signify-ts/src/keri/core/manager.ts @@ -0,0 +1,1324 @@ +import { Encrypter } from './encrypter.ts'; +import { Decrypter } from './decrypter.ts'; +import { Salter, Tier } from './salter.ts'; +import { Signer } from './signer.ts'; +import { Verfer } from './verfer.ts'; +import { MtrDex } from './matter.ts'; +import { Diger } from './diger.ts'; +import { Cigar } from './cigar.ts'; +import { Siger } from './siger.ts'; +import { b } from './core.ts'; + +/** + * Kinds of key pair generation algorithms. + * Salty is deterministic based on a salt and stem. + * Randy is random. + * Group is a multi-signature group algorithm indicating keys are retrieved from a group member which will be either Salty or Randy. + * Extern is an external key pair algorithm indicating keys are provided by an external source such as an HSM. + */ +export enum Algos { + randy = 'randy', + salty = 'salty', + group = 'group', + extern = 'extern', +} + +/** + * Lot (set) of public keys as an ordered list with indexes and the time created. + * Indexes refer to the index of the ordered sequence of keys in an establishment event (inception or rotation). + * Assumes the same length for each set of keys across all PubLot instances used for a given identifier. + */ +class PubLot { + /** + * List of fully qualified, Base64 encoded public keys. Defaults to empty. + */ + public pubs: Array = new Array(); + /** + * Rotation index; index of rotation (est event) that uses this public key set. + * The index of the key set for an inception event is 0. + */ + public ridx: number = 0; + /** + * Key index; index of the starting key in the key set for this lot in sequence + * with reference to all public keys for the identifier. + * For example, if each set (PubLot.pubs) has 3 keys then ridx 2 has kidx of 2*3 = 6. + * Defaults to 0. + */ + public kidx: number = 0; + /** + * Datetime of when key set created formatted as an ISO 8601 compliant string. + */ + public dt: string = ''; +} + +/** + * Prefix's public key situation (set of public keys). + */ +class PreSit { + /** + * Previous publot; previous public key set. + */ + public old: PubLot = new PubLot(); + /** + * New, current publot; current public key set. + */ + public new: PubLot = new PubLot(); + /** + * Next public publot + */ + public nxt: PubLot = new PubLot(); +} + +/** + * Identifier prefix parameters for creating new key pairs. + */ +class PrePrm { + /** + * Prefix index for this keypair sequence. + */ + public pidx: number = 0; + /** + * Key generation algorithm type. + * Defaults to Salty. + * Salty default uses indices and salt to create new key pairs. + */ + public algo: Algos = Algos.salty; + /** + * Used for salty algo. Defaults to empty. Unused for randy algo. + */ + public salt: string = ''; + /** + * Default unique path prefix used by the salty algo during key generation. + */ + public stem: string = ''; + /** + * Security tier for stretch index; used by the salty algo. + */ + public tier: string = ''; +} + +/** + * An identifier prefix's public key set (list) at a given rotation index ridx. + */ +class PubSet { + /** + * List of fully qualified, Base64 encoded public keys. + */ + pubs: Array = new Array(); +} + +/** + * Describes a path to a specific derived keypair for a given identifier + */ +class PubPath { + /** + * The path to a specific keypair. To generate a keypair you combine the path with the salt and tier. + */ + path: string = ''; + /** + * Derivation code indicating the kind of cryptographic keypair to generate. Defaults to Ed25519. + */ + code: string = ''; + /** + * Security tier to use to generate a keypair. Defaults to high. + */ + tier: string = Tier.high; + /** + * Flag to control whether to generate a low security, temporary key. Used for speed for unit tests. Do NOT use for production identifiers. + */ + temp: boolean = false; +} + +class Keys { + private readonly _signers: Array; + private readonly _paths?: Array; + + constructor(signers: Array, paths?: Array) { + this._signers = signers; + if (paths != undefined) { + if (signers.length != paths.length) { + throw new Error( + 'If paths are provided, they must be the same length as signers' + ); + } + } + this._paths = paths; + } + + get paths(): Array | undefined { + return this._paths; + } + + get signers(): Array { + return this._signers; + } +} + +/** + * Interface for creating a key pair based on an algorithm. + */ +export interface Creator { + /** + * Creates a key pair + * @param codes list of derivation codes one per key pair to create + * @param count count of key pairs to create if codes not provided + * @param code derivation code to use for count key pairs if codes not provided + * @param transferable true means use transferable derivation code. Otherwise, non-transferable derivation code. + * @param pidx prefix index for this keypair sequence + * @param ridx rotation index for this key pair set + * @param kidx starting key index for this key pair set + * @param temp true means use temp stretch otherwise use time set by tier for streching + */ + create( + codes: Array | undefined, + count: number, + code: string, + transferable: boolean, + pidx: number, + ridx: number, + kidx: number, + temp: boolean + ): Keys; + + /** + * Salt used for key pair generation. + * Used only for Salty key creation. + */ + salt: string; + /** + * String prefix used to stretch the prefix, salt, and seed into the key pair. + * Used only for Salty key creation. + */ + stem: string; + /** + * Security tier used during stretching. + */ + tier: Tier; +} + +export class RandyCreator implements Creator { + create( + codes: Array | undefined = undefined, + count: number = 1, + code: string = MtrDex.Ed25519_Seed, + transferable: boolean = true + ): Keys { + const signers = new Array(); + if (codes == undefined) { + codes = new Array(count).fill(code); + } + + codes.forEach(function (code) { + signers.push( + new Signer({ code: code, transferable: transferable }) + ); + }); + + return new Keys(signers); + } + + /** + * Unused for random key generation. + */ + get salt(): string { + return ''; + } + + /** + * Unused for random key generation. + */ + get stem(): string { + return ''; + } + + /** + * Unused for random key generation. + */ + get tier(): Tier { + return '' as Tier; + } +} + +/** + * Deterministically creates a key pair based on combining a salt with a path stretch algorithm. + * The salt is randomized if not provided. + */ +export class SaltyCreator implements Creator { + /** + * The salter used to create the key pair. Contains the private key. + */ + public salter: Salter; + /** + * Key material prefix used during key stretching. + * @private + */ + private readonly _stem: string; + constructor( + salt: string | undefined = undefined, + tier: Tier | undefined = undefined, + stem: string | undefined = undefined + ) { + this.salter = new Salter({ qb64: salt, tier: tier }); + this._stem = stem == undefined ? '' : stem; + } + + get salt(): string { + return this.salter.qb64; + } + + get stem(): string { + return this._stem; + } + + get tier(): Tier { + return this.salter.tier!; + } + + create( + codes: Array | undefined = undefined, + count: number = 1, + code: string = MtrDex.Ed25519_Seed, + transferable: boolean = true, + pidx: number = 0, + ridx: number = 0, + kidx: number = 0, + temp: boolean = false + ): Keys { + const signers = new Array(); + const paths = new Array(); + + if (codes == undefined) { + codes = new Array(count).fill(code); + } + + codes.forEach((code, idx) => { + // Previous definition of path + // let path = this.stem + pidx.toString(16) + ridx.toString(16) + (kidx+idx).toString(16) + const path = + this.stem == '' + ? pidx.toString(16) + : this.stem + ridx.toString(16) + (kidx + idx).toString(16); + + signers.push( + this.salter.signer(code, transferable, path, this.tier, temp) + ); + paths.push(path); + }); + + return new Keys(signers, paths); + } +} + +export class Creatory { + private readonly _make: any; + constructor(algo: Algos = Algos.salty) { + switch (algo) { + case Algos.randy: + this._make = this._makeRandy; + break; + case Algos.salty: + this._make = this._makeSalty; + break; + default: + throw new Error(`unsupported algo=${algo}`); + } + } + + make(...args: any[]): Creator { + return this._make(...args); + } + + _makeRandy(): Creator { + return new RandyCreator(); + } + + _makeSalty(...args: any[]): Creator { + return new SaltyCreator(...args); + } +} + +export function openManager(passcode: string, salt?: string) { + if (passcode.length < 21) { + throw new Error('Bran (passcode seed material) too short.'); + } + + const bran = MtrDex.Salt_128 + 'A' + passcode.substring(0, 21); // qb64 salt for seed + const signer = new Salter({ qb64: bran }).signer( + MtrDex.Ed25519_Seed, + false + ); + const seed = signer.qb64; + const aeid = signer.verfer.qb64; // lest it remove encryption + + let algo; + + const salter = salt != undefined ? new Salter({ qb64: salt }) : undefined; + if (salt != undefined) { + algo = Algos.salty; + } else { + algo = Algos.randy; + } + + return new Manager({ seed: seed, aeid: aeid, algo: algo, salter: salter }); +} + +export interface ManagerArgs { + ks?: KeyStore | undefined; + seed?: string | undefined; + aeid?: string | undefined; + pidx?: number | undefined; + algo?: Algos | undefined; + salter?: Salter | undefined; + tier?: string | undefined; +} + +export interface ManagerInceptArgs { + icodes?: any | undefined; + icount?: number; + icode?: string; + ncodes?: any | undefined; + ncount?: number; + ncode?: string; + dcode?: string; + algo?: Algos | undefined; + salt?: string | undefined; + stem?: string | undefined; + tier?: string | undefined; + rooted?: boolean; + transferable?: boolean; + temp?: boolean; +} + +interface RotateArgs { + pre: string; + ncodes?: any | undefined; + ncount?: number; + ncode?: string; + dcode?: string; + transferable?: boolean; + temp?: boolean; + erase?: boolean; +} + +interface SignArgs { + ser: Uint8Array; + pubs?: Array | undefined; + verfers?: Array | undefined; + indexed?: boolean; + indices?: Array | undefined; + ondices?: Array | undefined; +} + +/** + * Manages key pair creation, retrieval, and message signing. + */ +export class Manager { + private _seed?: string; + private _salt?: string; + private _encrypter: Encrypter | undefined; + private _decrypter: Decrypter | undefined; + private readonly _ks: KeyStore; + + constructor(args: ManagerArgs) { + let { ks, seed, aeid, pidx, algo, salter, tier } = args; + this._ks = ks == undefined ? new Keeper() : ks; + this._seed = seed; + this._encrypter = undefined; + this._decrypter = undefined; + + aeid = aeid == undefined ? undefined : aeid; + pidx = pidx == undefined ? 0 : pidx; + algo = algo == undefined ? Algos.salty : algo; + + const salt = salter?.qb64; + + tier = tier == undefined ? Tier.low : tier; + + if (this.pidx == undefined) { + this.pidx = pidx; + } + + if (this.algo == undefined) { + this.algo = algo; + } + + if (this.salt == undefined) { + this.salt = salt; + } + + if (this.tier == undefined) { + this.tier = tier; + } + + if (this.aeid == undefined) { + this.updateAeid(aeid, this._seed); + } + } + + get ks(): KeyStore { + return this._ks; + } + + get encrypter(): Encrypter | undefined { + return this._encrypter; + } + + get decrypter(): Decrypter | undefined { + return this._decrypter; + } + + get seed(): string | undefined { + return this._seed; + } + + /** + * qb64 auth encrypt id prefix + */ + get aeid(): string | undefined { + return this.ks.getGbls('aeid'); + } + + get pidx(): number | undefined { + const pidx = this.ks.getGbls('pidx'); + if (pidx != undefined) { + return parseInt(pidx, 16); + } + return undefined; + } + + set pidx(pidx: number | undefined) { + this.ks.pinGbls('pidx', pidx!.toString(16)); + } + + get salt(): string | undefined { + if (this._decrypter == undefined) { + return this._salt; + } else { + const salt = this.ks.getGbls('salt'); + return this._decrypter.decrypt(b(salt)).qb64; + } + } + + set salt(salt: string | undefined) { + if (this._encrypter == undefined) { + this._salt = salt; + } else { + salt = this._encrypter.encrypt(b(salt)).qb64; + this.ks.pinGbls('salt', salt!); + } + } + + get tier(): string | undefined { + return this.ks.getGbls('tier'); + } + + set tier(tier: string | undefined) { + this.ks.pinGbls('tier', tier!); + } + + get algo(): Algos | undefined { + const a = this.ks.getGbls('algo'); + const ta = a as keyof typeof Algos; + return Algos[ta]; + } + + set algo(algo: Algos | undefined) { + this.ks.pinGbls('algo', algo! as string); + } + + private updateAeid(aeid: string | undefined, seed?: string) { + if (this.aeid != undefined) { + const seed = b(this._seed); + if (this._seed == undefined || !this._encrypter?.verifySeed(seed)) { + throw new Error(`Last seed missing or provided last seed " + "not associated with last aeid=${this.aeid}.`); + } + } + + if (aeid != '' && aeid != undefined) { + if (aeid != this.aeid) { + this._encrypter = new Encrypter({}, b(aeid)); + if (seed == undefined || !this._encrypter.verifySeed(b(seed))) { + throw new Error(`Seed missing or provided seed not associated" + " with provided aeid=${aeid}.`); + } + } + } else if (this.algo == Algos.randy) { + // Unlike KERIpy, we don't support unencrypted secrets + throw new Error( + 'Invalid Manager configuration, encryption must be used with Randy key creation.' + ); + } else { + this._encrypter = undefined; + } + + const salt = this.salt; + if (salt != undefined) { + this.salt = salt; + } + + if (this._decrypter != undefined) { + for (const [keys, data] of this.ks.prmsElements()) { + if (data.salt != undefined) { + const salter = this._decrypter.decrypt(b(data.salt)); + data.salt = + this._encrypter == undefined + ? salter.qb64 + : this._encrypter.encrypt(null, salter); + this.ks.pinPrms(keys, data); + } + } + + for (const [pubKey, signer] of this.ks.prisElements( + this._decrypter + )) { + this.ks.pinPris(pubKey, signer, this._encrypter!); + } + } + + this.ks.pinGbls('aeid', aeid!); // set aeid in db + this._seed = seed; // set .seed in memory + + // update .decrypter + this._decrypter = + seed != undefined ? new Decrypter({}, b(seed)) : undefined; + } + + incept(args: ManagerInceptArgs): [Array, Array] { + let { + icodes = undefined, + icount = 1, + icode = MtrDex.Ed25519_Seed, + ncodes = undefined, + ncount = 1, + ncode = MtrDex.Ed25519_Seed, + dcode = MtrDex.Blake3_256, + algo = undefined, + salt = undefined, + stem = undefined, + tier = undefined, + rooted = true, + transferable = true, + temp = false, + } = args; + if (rooted && algo == undefined) { + algo = this.algo; + } + if (rooted && salt == undefined) { + salt = this.salt; + } + if (rooted && tier == undefined) { + tier = this.tier; + } + + const pidx = this.pidx!; + const ridx = 0; + const kidx = 0; + + const creator = new Creatory(algo).make(salt, tier, stem); + + if (icodes == undefined) { + if (icount < 0) { + throw new Error(`Invalid icount=${icount} must be >= 0.`); + } + + icodes = new Array(icount).fill(icode); + } + + const ikeys = creator.create( + icodes, + 0, + MtrDex.Ed25519_Seed, + transferable, + pidx, + ridx, + kidx, + temp + ); + const verfers = Array.from( + ikeys.signers, + (signer: Signer) => signer.verfer + ); + + if (ncodes == undefined) { + if (ncount < 0) { + throw new Error(`Invalid ncount=${ncount} must be >= 0.`); + } + + ncodes = new Array(ncount).fill(ncode); + } + + const nkeys = creator.create( + ncodes, + 0, + MtrDex.Ed25519_Seed, + transferable, + pidx, + ridx + 1, + kidx + icodes.length, + temp + ); + + const digers = Array.from( + nkeys.signers, + (signer: Signer) => new Diger({ code: dcode }, signer.verfer.qb64b) + ); + + const pp = new PrePrm(); + pp.pidx = pidx!; + pp.algo = algo!; + pp.salt = + creator.salt.length == 0 || this.encrypter == undefined + ? '' + : this.encrypter.encrypt(b(creator.salt)).qb64; + pp.stem = creator.stem; + pp.tier = creator.tier; + + const dt = new Date().toString(); + const nw = new PubLot(); + nw.pubs = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + nw.ridx = ridx; + nw.kidx = kidx; + nw.dt = dt; + + const nt = new PubLot(); + nt.pubs = Array.from( + nkeys.signers, + (signer: Signer) => signer.verfer.qb64 + ); + nt.ridx = ridx + 1; + nt.kidx = kidx + icodes.length; + nt.dt = dt; + + const ps = new PreSit(); + ps.new = nw; + ps.nxt = nt; + + const pre = verfers[0].qb64; + if (!this.ks.putPres(pre, verfers[0].qb64b)) { + throw new Error(`Already incepted pre=${pre}.`); + } + + if (!this.ks.putPrms(pre, pp)) { + throw new Error(`Already incepted prm for pre=${pre}.`); + } + + this.pidx = pidx! + 1; + + if (!this.ks.putSits(pre, ps)) { + throw new Error(`Already incepted sit for pre=${pre}.`); + } + + if (this.encrypter != undefined) { + // Only store encrypted keys if we have an encrypter, otherwise regenerate + ikeys.signers.forEach((signer: Signer) => { + this.ks.putPris(signer.verfer.qb64, signer, this.encrypter!); + }); + + nkeys.signers.forEach((signer: Signer) => { + this.ks.putPris(signer.verfer.qb64, signer, this.encrypter!); + }); + } else if ( + this._encrypter == undefined && + ikeys.paths != undefined && + nkeys.paths != undefined + ) { + ikeys.paths.forEach((path: string, idx: number) => { + const signer = ikeys.signers[idx]; + const ppt = new PubPath(); + ppt.path = path; + ppt.code = icodes[idx]; + ppt.tier = pp.tier; + ppt.temp = temp; + this.ks.putPths(signer.verfer.qb64, ppt); + }); + nkeys.paths.forEach((path: string, idx: number) => { + const signer = nkeys.signers[idx]; + const ppt = new PubPath(); + ppt.path = path; + ppt.code = ncodes[idx]; + ppt.tier = pp.tier; + ppt.temp = temp; + this.ks.putPths(signer.verfer.qb64, ppt); + }); + } else { + throw new Error( + 'invalid configuration, randy keys without encryption' + ); + } + + const pubSet = new PubSet(); + pubSet.pubs = ps.new.pubs; + this.ks.putPubs(riKey(pre, ridx), pubSet); + + const nxtPubSet = new PubSet(); + nxtPubSet.pubs = ps.nxt.pubs; + this.ks.putPubs(riKey(pre, ridx + 1), nxtPubSet); + + return [verfers, digers]; + } + + move(old: string, gnu: string) { + if (old == gnu) { + return; + } + + if (this.ks.getPres(old) == undefined) { + throw new Error(`Nonexistent old pre=${old}, nothing to assign.`); + } + + if (this.ks.getPres(gnu) != undefined) { + throw new Error(`Preexistent new pre=${gnu} may not clobber.`); + } + + const oldprm = this.ks.getPrms(old); + if (oldprm == undefined) { + throw new Error( + `Nonexistent old prm for pre=${old}, nothing to move.` + ); + } + + if (this.ks.getPrms(gnu) != undefined) { + throw new Error( + `Preexistent new prm for pre=${gnu} may not clobber.` + ); + } + + const oldsit = this.ks.getSits(old); + if (oldsit == undefined) { + throw new Error( + `Nonexistent old sit for pre=${old}, nothing to move.` + ); + } + + if (this.ks.getSits(gnu) != undefined) { + throw new Error( + `Preexistent new sit for pre=${gnu} may not clobber.` + ); + } + + if (!this.ks.putPrms(gnu, oldprm)) { + throw new Error( + `Failed moving prm from old pre=${old} to new pre=${gnu}.` + ); + } else { + this.ks.remPrms(old); + } + + if (!this.ks.putSits(gnu, oldsit)) { + throw new Error( + `Failed moving sit from old pre=${old} to new pre=${gnu}.` + ); + } else { + this.ks.remSits(old); + } + + let i = 0; + while (true) { + const pl = this.ks.getPubs(riKey(old, i)); + if (pl == undefined) { + break; + } + + if (!this.ks.putPubs(riKey(gnu, i), pl)) { + throw new Error( + `Failed moving pubs at pre=${old} ri=${i} to new pre=${gnu}` + ); + } + i = i + 1; + } + + if (!this.ks.pinPres(old, b(gnu))) { + throw new Error( + `Failed assiging new pre=${gnu} to old pre=${old}.` + ); + } + + if (!this.ks.putPres(gnu, b(gnu))) { + throw new Error(`Failed assiging new pre=${gnu}.`); + } + } + + rotate(args: RotateArgs): [Array, Array] { + let { + pre, + ncodes = undefined, + ncount = 1, + ncode = MtrDex.Ed25519_Seed, + dcode = MtrDex.Blake3_256, + transferable = true, + temp = false, + erase = true, + } = args; + const pp = this.ks.getPrms(pre); + if (pp == undefined) { + throw new Error(`Attempt to rotate nonexistent pre=${pre}.`); + } + + const ps = this.ks.getSits(pre); + if (ps == undefined) { + throw new Error(`Attempt to rotate nonexistent pre=${pre}.`); + } + + if (ps.nxt.pubs == undefined || ps.nxt.pubs.length == 0) { + throw new Error(`Attempt to rotate nontransferable pre=${pre}.`); + } + + const old = ps.old; + ps.old = ps.new; + ps.new = ps.nxt; + + if (this.aeid != undefined && this.decrypter == undefined) { + throw new Error( + 'Unauthorized decryption attempt. Aeid but no decrypter.' + ); + } + + const verfers = new Array(); + ps.new.pubs.forEach((pub) => { + if (this.decrypter != undefined) { + const signer = this.ks.getPris(pub, this.decrypter); + if (signer == undefined) { + throw new Error(`Missing prikey in db for pubkey=${pub}`); + } + verfers.push(signer.verfer); + } else { + // Should we regenerate from salt here since this.decryptor is undefined + verfers.push(new Verfer({ qb64: pub })); + } + }); + + let salt = pp.salt; + if (salt != undefined && salt != '') { + // If you provded a Salt for an AID but don't have encryption, pitch a fit + if (this.decrypter == undefined) { + throw new Error( + 'Invalid configuration: AID salt with no encryption' + ); + } + salt = this.decrypter.decrypt(b(salt)).qb64; + } else { + salt = this.salt!; + } + + const creator = new Creatory(pp.algo).make(salt, pp.tier, pp.stem); + + if (ncodes == undefined) { + if (ncount < 0) { + throw new Error(`Invalid count=${ncount} must be >= 0`); + } + ncodes = new Array(ncount).fill(ncode); + } + + const pidx = pp.pidx; + const ridx = ps.new.ridx + 1; + const kidx = ps.nxt.kidx + ps.new.pubs.length; + + const keys = creator.create( + ncodes, + 0, + '', + transferable, + pidx, + ridx, + kidx, + temp + ); + const digers = Array.from( + keys.signers, + (signer: Signer) => new Diger({ code: dcode }, signer.verfer.qb64b) + ); + + const dt = new Date().toString(); + ps.nxt = new PubLot(); + ps.nxt.pubs = Array.from( + keys.signers, + (signer: Signer) => signer.verfer.qb64 + ); + ps.nxt.ridx = ridx; + ps.nxt.kidx = kidx; + ps.nxt.dt = dt; + + if (!this.ks.pinSits(pre, ps)) { + throw new Error(`Problem updating pubsit db for pre=${pre}.`); + } + + if (this.encrypter != undefined) { + // Only store encrypted keys if we have an encrypter, otherwise regenerate + keys.signers.forEach((signer: Signer) => { + this.ks.putPris(signer.verfer.qb64, signer, this.encrypter!); + }); + } else if (this._encrypter == undefined && keys.paths != undefined) { + keys.paths.forEach((path: string, idx: number) => { + const signer = keys.signers[idx]; + const ppt = new PubPath(); + ppt.path = path; + ppt.tier = pp!.tier; + ppt.temp = temp; + this.ks.putPths(signer.verfer.qb64, ppt); + }); + } else { + throw new Error( + 'invalid configuration, randy keys without encryption' + ); + } + + const newPs = new PubSet(); + newPs.pubs = ps.nxt.pubs; + this.ks.putPubs(riKey(pre, ps.nxt.ridx), newPs); + + if (erase) { + old.pubs.forEach((pub) => { + this.ks.remPris(pub); + }); + } + + return [verfers, digers]; + } + + sign(args: SignArgs) { + const { + ser, + pubs = undefined, + verfers = undefined, + indexed = true, + indices = undefined, + ondices = undefined, + } = args; + const signers = new Array(); + + if (pubs == undefined && verfers == undefined) { + throw new Error('pubs or verfers required'); + } + + if (pubs != undefined) { + if (this.aeid != undefined && this.decrypter == undefined) { + throw new Error( + 'Unauthorized decryption attempt. Aeid but no decrypter.' + ); + } + + pubs.forEach((pub) => { + //If no decrypter then get SaltyState and regenerate prikey + if (this.decrypter != undefined) { + const signer = this.ks.getPris(pub, this.decrypter); + if (signer == undefined) { + throw new Error( + `Missing prikey in db for pubkey=${pub}` + ); + } + signers.push(signer); + } else { + const verfer = new Verfer({ qb64: pub }); + const ppt = this.ks.getPths(pub); + if (ppt == undefined) { + throw new Error( + `Missing prikey in db for pubkey=${pub}` + ); + } + const salter = new Salter({ qb64: this.salt }); + signers.push( + salter.signer( + ppt.code, + verfer.transferable, + ppt.path, + ppt.tier as Tier, + ppt.temp + ) + ); + } + }); + } else { + verfers!.forEach((verfer: Verfer) => { + if (this.decrypter != undefined) { + const signer = this.ks.getPris(verfer.qb64, this.decrypter); + if (signer == undefined) { + throw new Error( + `Missing prikey in db for pubkey=${verfer.qb64}` + ); + } + signers.push(signer); + } else { + const ppt = this.ks.getPths(verfer.qb64); + if (ppt == undefined) { + throw new Error( + `Missing prikey in db for pubkey=${verfer.qb64}` + ); + } + const salter = new Salter({ qb64: this.salt }); + signers.push( + salter.signer( + ppt.code, + verfer.transferable, + ppt.path, + ppt.tier as Tier, + ppt.temp + ) + ); + } + }); + } + + if (indices != undefined && indices.length != signers.length) { + throw new Error( + `Mismatch indices length=${indices.length} and resultant signers length=${signers.length}` + ); + } + + if (ondices != undefined && ondices.length != signers.length) { + throw new Error( + `Mismatch ondices length=${ondices.length} and resultant signers length=${signers.length}` + ); + } + + if (indexed) { + const sigers = new Array(); + signers.forEach((signer, idx) => { + let i; + if (indices != undefined) { + i = indices[idx]; + if (i < 0) { + throw new Error( + `Invalid signing index = ${i}, not whole number.` + ); + } + } else { + i = idx; + } + + let o; + if (ondices != undefined) { + o = ondices[idx]; + if (o <= 0) { + throw new Error( + `Invalid other signing index = {o}, not None or not whole number.` + ); + } + } else { + o = i; + } + + const only = o == undefined; + sigers.push(signer.sign(ser, i, only, o) as Siger); + }); + return sigers; + } else { + const cigars = new Array(); + signers.forEach((signer: Signer) => { + cigars.push(signer.sign(ser) as Cigar); + }); + + return cigars; + } + } +} + +export function riKey(pre: string, ridx: number) { + return pre + '.' + ridx.toString(16).padStart(32, '0'); +} + +/** + * Sub interface for key store specific functions. + */ +export interface KeyStore { + getGbls(key: string): string | undefined; + pinGbls(key: string, val: string): void; + + prmsElements(): Array<[string, PrePrm]>; + getPrms(keys: string): PrePrm | undefined; + pinPrms(keys: string, data: PrePrm): void; + putPrms(keys: string, data: PrePrm): boolean; + remPrms(keys: string): boolean; + + prisElements(decrypter: Decrypter): Array<[string, Signer]>; + getPris(keys: string, decrypter: Decrypter): Signer | undefined; + pinPris(keys: string, data: Signer, encrypter: Encrypter): void; + putPris(pubKey: string, signer: Signer, encrypter: Encrypter): boolean; + remPris(pubKey: string): void; + + getPths(pubKey: string): PubPath | undefined; + putPths(pubKey: string, val: PubPath): boolean; + pinPths(pubKey: string, val: PubPath): boolean; + + getPres(pre: string): Uint8Array | undefined; + putPres(pre: string, val: Uint8Array): boolean; + pinPres(pre: string, val: Uint8Array): boolean; + + getSits(keys: string): PreSit | undefined; + putSits(pre: string, val: PreSit): boolean; + pinSits(pre: string, val: PreSit): boolean; + remSits(keys: string): boolean; + + getPubs(keys: string): PubSet | undefined; + putPubs(keys: string, data: PubSet): boolean; +} + +/** + * Keeper sets up named sub databases for key pair storage (KS). + * Methods provide key pair creation, storage, and data signing. + * In-memory test implementation of Keeper key store + */ +class Keeper implements KeyStore { + private readonly _gbls: Map; + private readonly _pris: Map; + private readonly _pths: Map; + private readonly _pres: Map; + private readonly _prms: Map; + private readonly _sits: Map; + private readonly _pubs: Map; + + constructor() { + this._gbls = new Map(); + this._pris = new Map(); + this._pths = new Map(); + this._pres = new Map(); + this._prms = new Map(); + this._sits = new Map(); + this._pubs = new Map(); + } + + getGbls(key: string): string | undefined { + return this._gbls.get(key); + } + + pinGbls(key: string, val: string): void { + this._gbls.set(key, val); + } + + prmsElements(): Array<[string, PrePrm]> { + const out = new Array<[string, PrePrm]>(); + this._prms.forEach((value, key) => { + out.push([key, value]); + }); + + return out; + } + + getPrms(keys: string): PrePrm | undefined { + return this._prms.get(keys); + } + + pinPrms(keys: string, data: PrePrm): void { + this._prms.set(keys, data); + } + + putPrms(keys: string, data: PrePrm): boolean { + if (this._prms.has(keys)) { + return false; + } + this._prms.set(keys, data); + return true; + } + + remPrms(keys: string): boolean { + return this._prms.delete(keys); + } + + prisElements(decrypter: Decrypter): Array<[string, Signer]> { + const out = new Array<[string, Signer]>(); + this._pris.forEach(function (val, pubKey) { + const verfer = new Verfer({ qb64: pubKey }); + const signer = decrypter.decrypt(val, null, verfer.transferable); + out.push([pubKey, signer]); + }); + return out; + } + + pinPris(pubKey: string, signer: Signer, encrypter: Encrypter): void { + const cipher = encrypter.encrypt(null, signer); + this._pris.set(pubKey, cipher.qb64b); + } + + putPris(pubKey: string, signer: Signer, encrypter: Encrypter): boolean { + if (this._pris.has(pubKey)) { + return false; + } + const cipher = encrypter.encrypt(null, signer); + this._pris.set(pubKey, cipher.qb64b); + return true; + } + + getPris(pubKey: string, decrypter: Decrypter): Signer | undefined { + const val = this._pris.get(pubKey); + if (val == undefined) { + return undefined; + } + const verfer = new Verfer({ qb64: pubKey }); + + return decrypter.decrypt(val, null, verfer.transferable); + } + + pinPths(pubKey: string, val: PubPath): boolean { + this._pths.set(pubKey, val); + return true; + } + + putPths(pubKey: string, val: PubPath): boolean { + if (this._pths.has(pubKey)) { + return false; + } + + this._pths.set(pubKey, val); + return true; + } + + getPths(pubKey: string): PubPath | undefined { + return this._pths.get(pubKey); + } + + remPris(pubKey: string): void { + this._pris.delete(pubKey); + } + + getPres(pre: string): Uint8Array | undefined { + return this._pres.get(pre); + } + + pinPres(pre: string, val: Uint8Array): boolean { + this._pres.set(pre, val); + return true; + } + + putPres(pre: string, val: Uint8Array): boolean { + if (this._pres.has(pre)) { + return false; + } + + this._pres.set(pre, val); + return true; + } + + getSits(keys: string): PreSit | undefined { + return this._sits.get(keys); + } + + putSits(pre: string, val: PreSit): boolean { + if (this._sits.has(pre)) { + return false; + } + + this._sits.set(pre, val); + return true; + } + + pinSits(pre: string, val: PreSit): boolean { + this._sits.set(pre, val); + return true; + } + + remSits(keys: string): boolean { + return this._sits.delete(keys); + } + + getPubs(keys: string): PubSet | undefined { + return this._pubs.get(keys); + } + + putPubs(keys: string, data: PubSet): boolean { + if (this._pubs.has(keys)) { + return false; + } + this._pubs.set(keys, data); + return true; + } +} diff --git a/packages/signify-ts/src/keri/core/matter.ts b/packages/signify-ts/src/keri/core/matter.ts new file mode 100644 index 00000000..76890e3e --- /dev/null +++ b/packages/signify-ts/src/keri/core/matter.ts @@ -0,0 +1,520 @@ +import { EmptyMaterialError } from './kering.ts'; + +import { intToB64, readInt } from './core.ts'; +import { b, d } from './core.ts'; +import { decodeBase64Url, encodeBase64Url } from './base64.ts'; + +export class Codex { + has(prop: string): boolean { + const m = new Map( + Array.from(Object.entries(this), (v) => [v[1], v[0]]) + ); + return m.has(prop); + } +} + +export class MatterCodex extends Codex { + Ed25519_Seed: string = 'A'; // Ed25519 256 bit random seed for private key + Ed25519N: string = 'B'; // Ed25519 verification key non-transferable, basic derivation. + X25519: string = 'C'; // X25519 public encryption key, converted from Ed25519 or Ed25519N. + Ed25519: string = 'D'; // Ed25519 verification key basic derivation + Blake3_256: string = 'E'; // Blake3 256 bit digest self-addressing derivation. + SHA3_256: string = 'H'; // SHA3 256 bit digest self-addressing derivation. + SHA2_256: string = 'I'; // SHA2 256 bit digest self-addressing derivation. + ECDSA_256k1_Seed: string = 'J'; // ECDSA secp256k1 256 bit random Seed for private key + X25519_Private: string = 'O'; // X25519 private decryption key converted from Ed25519 + X25519_Cipher_Seed: string = 'P'; // X25519 124 char b64 Cipher of 44 char qb64 Seed + ECDSA_256r1_Seed: string = 'Q'; // ECDSA secp256r1 256 bit random Seed for private key + Salt_128: string = '0A'; // 128 bit random salt or 128 bit number (see Huge) + Ed25519_Sig: string = '0B'; // Ed25519 signature. + ECDSA_256k1_Sig: string = '0C'; // ECDSA secp256k1 signature. + ECDSA_256r1_Sig: string = '0I'; // ECDSA secp256r1 signature. + StrB64_L0: string = '4A'; // String Base64 Only Lead Size 0 + StrB64_L1: string = '5A'; // String Base64 Only Lead Size 1 + StrB64_L2: string = '6A'; // String Base64 Only Lead Size 2 + ECDSA_256k1N: string = '1AAA'; // ECDSA secp256k1 verification key non-transferable, basic derivation. + ECDSA_256k1: string = '1AAB'; // ECDSA public verification or encryption key, basic derivation + X25519_Cipher_Salt: string = '1AAH'; // X25519 100 char b64 Cipher of 24 char qb64 Salt + ECDSA_256r1N: string = '1AAI'; // ECDSA secp256r1 verification key non-transferable, basic derivation. + ECDSA_256r1: string = '1AAJ'; // ECDSA secp256r1 verification or encryption key, basic derivation + StrB64_Big_L0: string = '7AAA'; // String Base64 Only Big Lead Size 0 + StrB64_Big_L1: string = '8AAA'; // String Base64 Only Big Lead Size 1 + StrB64_Big_L2: string = '9AAA'; // String Base64 Only Big Lead Size 2 +} + +export const MtrDex = new MatterCodex(); + +export class NonTransCodex extends Codex { + Ed25519N: string = 'B'; // Ed25519 verification key non-transferable, basic derivation. + ECDSA_256k1N: string = '1AAA'; // ECDSA secp256k1 verification key non-transferable, basic derivation. + Ed448N: string = '1AAC'; // Ed448 non-transferable prefix public signing verification key. Basic derivation. + ECDSA_256r1N: string = '1AAI'; // ECDSA secp256r1 verification key non-transferable, basic derivation. +} + +export const NonTransDex = new NonTransCodex(); + +export class DigiCodex extends Codex { + Blake3_256: string = 'E'; // Blake3 256 bit digest self-addressing derivation. + Blake2b_256: string = 'F'; // Blake2b 256 bit digest self-addressing derivation. + Blake2s_256: string = 'G'; // Blake2s 256 bit digest self-addressing derivation. + SHA3_256: string = 'H'; // SHA3 256 bit digest self-addressing derivation. + SHA2_256: string = 'I'; // SHA2 256 bit digest self-addressing derivation. + Blake3_512: string = '0D'; // Blake3 512 bit digest self-addressing derivation. + Blake2b_512: string = '0E'; // Blake2b 512 bit digest self-addressing derivation. + SHA3_512: string = '0F'; // SHA3 512 bit digest self-addressing derivation. + SHA2_512: string = '0G'; // SHA2 512 bit digest self-addressing derivation. +} + +export const DigiDex = new DigiCodex(); + +export class NumCodex extends Codex { + Short: string = 'M'; // Short 2 byte b2 number + Long: string = '0H'; // Long 4 byte b2 number + Big: string = 'N'; // Big 8 byte b2 number + Huge: string = '0A'; // Huge 16 byte b2 number (same as Salt_128) +} + +export const NumDex = new NumCodex(); + +export class BexCodex extends Codex { + StrB64_L0: string = '4A'; // String Base64 Only Leader Size 0 + StrB64_L1: string = '5A'; // String Base64 Only Leader Size 1 + StrB64_L2: string = '6A'; // String Base64 Only Leader Size 2 + StrB64_Big_L0: string = '7AAA'; // String Base64 Only Big Leader Size 0 + StrB64_Big_L1: string = '8AAA'; // String Base64 Only Big Leader Size 1 + StrB64_Big_L2: string = '9AAA'; // String Base64 Only Big Leader Size 2 +} + +export const BexDex = new BexCodex(); + +class SmallVarRawSizeCodex extends Codex { + Lead0: string = '4'; // First Selector Character for all ls == 0 codes + Lead1: string = '5'; // First Selector Character for all ls == 1 codes + Lead2: string = '6'; // First Selector Character for all ls == 2 codes +} + +export const SmallVrzDex = new SmallVarRawSizeCodex(); + +class LargeVarRawSizeCodex extends Codex { + Lead0_Big: string = '7'; // First Selector Character for all ls == 0 codes + Lead1_Big: string = '8'; // First Selector Character for all ls == 1 codes + Lead2_Big: string = '9'; // First Selector Character for all ls == 2 codes +} + +export const LargeVrzDex = new LargeVarRawSizeCodex(); + +export class Sizage { + public hs: number; + public ss: number; + public ls?: number; + public fs?: number; + + constructor(hs: number, ss: number, fs?: number, ls?: number) { + this.hs = hs; + this.ss = ss; + this.fs = fs; + this.ls = ls!; + } +} + +export interface MatterArgs { + raw?: Uint8Array | undefined; + code?: string; + qb64b?: Uint8Array | undefined; + qb64?: string; + qb2?: Uint8Array | undefined; + rize?: number; +} + +export class Matter { + static Sizes = new Map( + Object.entries({ + A: new Sizage(1, 0, 44, 0), + B: new Sizage(1, 0, 44, 0), + C: new Sizage(1, 0, 44, 0), + D: new Sizage(1, 0, 44, 0), + E: new Sizage(1, 0, 44, 0), + F: new Sizage(1, 0, 44, 0), + G: new Sizage(1, 0, 44, 0), + H: new Sizage(1, 0, 44, 0), + I: new Sizage(1, 0, 44, 0), + J: new Sizage(1, 0, 44, 0), + K: new Sizage(1, 0, 76, 0), + L: new Sizage(1, 0, 76, 0), + M: new Sizage(1, 0, 4, 0), + N: new Sizage(1, 0, 12, 0), + O: new Sizage(1, 0, 44, 0), + P: new Sizage(1, 0, 124, 0), + Q: new Sizage(1, 0, 44, 0), + '0A': new Sizage(2, 0, 24, 0), + '0B': new Sizage(2, 0, 88, 0), + '0C': new Sizage(2, 0, 88, 0), + '0D': new Sizage(2, 0, 88, 0), + '0E': new Sizage(2, 0, 88, 0), + '0F': new Sizage(2, 0, 88, 0), + '0G': new Sizage(2, 0, 88, 0), + '0H': new Sizage(2, 0, 8, 0), + '0I': new Sizage(2, 0, 88, 0), + '1AAA': new Sizage(4, 0, 48, 0), + '1AAB': new Sizage(4, 0, 48, 0), + '1AAC': new Sizage(4, 0, 80, 0), + '1AAD': new Sizage(4, 0, 80, 0), + '1AAE': new Sizage(4, 0, 56, 0), + '1AAF': new Sizage(4, 0, 8, 0), + '1AAG': new Sizage(4, 0, 36, 0), + '1AAH': new Sizage(4, 0, 100, 0), + '1AAI': new Sizage(4, 0, 48, 0), + '1AAJ': new Sizage(4, 0, 48, 0), + '2AAA': new Sizage(4, 0, 8, 1), + '3AAA': new Sizage(4, 0, 8, 2), + '4A': new Sizage(2, 2, undefined, 0), + '5A': new Sizage(2, 2, undefined, 1), + '6A': new Sizage(2, 2, undefined, 2), + '7AAA': new Sizage(4, 4, undefined, 0), + '8AAA': new Sizage(4, 4, undefined, 1), + '9AAA': new Sizage(4, 4, undefined, 2), + '4B': new Sizage(2, 2, undefined, 0), + '5B': new Sizage(2, 2, undefined, 1), + '6B': new Sizage(2, 2, undefined, 2), + '7AAB': new Sizage(4, 4, undefined, 0), + '8AAB': new Sizage(4, 4, undefined, 1), + '9AAB': new Sizage(4, 4, undefined, 2), + }) + ); + + static Hards = new Map([ + ['A', 1], + ['B', 1], + ['C', 1], + ['D', 1], + ['E', 1], + ['F', 1], + ['G', 1], + ['H', 1], + ['I', 1], + ['J', 1], + ['K', 1], + ['L', 1], + ['M', 1], + ['N', 1], + ['O', 1], + ['P', 1], + ['Q', 1], + ['R', 1], + ['S', 1], + ['T', 1], + ['U', 1], + ['V', 1], + ['W', 1], + ['X', 1], + ['Y', 1], + ['Z', 1], + ['a', 1], + ['b', 1], + ['c', 1], + ['d', 1], + ['e', 1], + ['f', 1], + ['g', 1], + ['h', 1], + ['i', 1], + ['j', 1], + ['k', 1], + ['l', 1], + ['m', 1], + ['n', 1], + ['o', 1], + ['p', 1], + ['q', 1], + ['r', 1], + ['s', 1], + ['t', 1], + ['u', 1], + ['v', 1], + ['w', 1], + ['x', 1], + ['y', 1], + ['z', 1], + ['0', 2], + ['1', 4], + ['2', 4], + ['3', 4], + ['4', 2], + ['5', 2], + ['6', 2], + ['7', 4], + ['8', 4], + ['9', 4], + ]); + + private _code: string = ''; + private _size: number = -1; + private _raw: Uint8Array = new Uint8Array(0); + + constructor({ + raw, + code = MtrDex.Ed25519N, + qb64b, + qb64, + qb2, + rize, + }: MatterArgs) { + let size = -1; + if (raw != undefined) { + if (code.length == 0) { + throw new Error( + 'Improper initialization need either (raw and code) or qb64b or qb64 or qb2.' + ); + } + + if (SmallVrzDex.has(code[0]) || LargeVrzDex.has(code[0])) { + if (rize !== undefined) { + if (rize < 0) + throw new Error( + `missing var raw size for code=${code}` + ); + } else { + rize = raw.length; + } + + const ls = (3 - (rize % 3)) % 3; // calc actual lead (pad) size + size = Math.floor((rize + ls) / 3); // calculate value of size in triplets + if (SmallVrzDex.has(code[0])) { + if (size <= 64 ** 2 - 1) { + const hs = 2; + const s = Object.values(SmallVrzDex)[ls]; + code = `${s}${code.substring(1, hs)}`; + } else if (size <= 64 ** 4 - 1) { + const hs = 4; + const s = Object.values(LargeVrzDex)[ls]; + code = `${s}${'AAAA'.substring(0, hs - 2)}${code[1]}`; + } else { + throw new Error( + `Unsupported raw size for code=${code}` + ); + } + } else { + if (size <= 64 ** 4 - 1) { + const hs = 4; + const s = Object.values(LargeVrzDex)[ls]; + code = `${s}${code.substring(1, hs)}`; + } else { + throw new Error( + `Unsupported raw size for code=${code}` + ); + } + } + } else { + const sizage = Matter.Sizes.get(code); + if (sizage!.fs == -1) { + // invalid + throw new Error(`Unsupported variable size code=${code}`); + } + + rize = Matter._rawSize(code); + } + raw = raw.slice(0, rize); // copy only exact size from raw stream + if (raw.length != rize) { + // forbids shorter + throw new Error( + `Not enougth raw bytes for code=${code} expected ${rize} got ${raw.length}.` + ); + } + + this._code = code; // hard value part of code + this._size = size; // soft value part of code in int + this._raw = raw; // crypto ops require bytes not bytearray + } else if (qb64 !== undefined) { + this._exfil(qb64); + } else if (qb64b !== undefined) { + const qb64 = d(qb64b); + this._exfil(qb64); + } else if (qb2 !== undefined) { + this._bexfil(qb2); + } else { + throw new EmptyMaterialError('EmptyMaterialError'); + } + } + + get code(): string { + return this._code; + } + + get size() { + return this._size; + } + + get raw(): Uint8Array { + return this._raw; + } + + get qb64() { + return this._infil(); + } + + get qb64b() { + return b(this.qb64); + } + + get transferable(): boolean { + return !NonTransDex.has(this.code); + } + + get digestive(): boolean { + return DigiDex.has(this.code); + } + + static _rawSize(code: string) { + const sizage = this.Sizes.get(code); // get sizes + const cs = sizage!.hs + sizage!.ss; // both hard + soft code size + if (sizage!.fs === -1) { + throw Error(`Non-fixed raw size code ${code}.`); + } + + return Math.floor(((sizage!.fs! - cs) * 3) / 4) - sizage!.ls!; + } + + static _leadSize(code: string) { + const sizage = this.Sizes.get(code); + return sizage!.ls; + } + + get both() { + const sizage = Matter.Sizes.get(this.code); + return `${this.code}${intToB64(this.size, sizage!.ss)}`; + } + + private _infil() { + const code = this.code; + const size = this.size; + const raw = this.raw; + + const ps = (3 - (raw.length % 3)) % 3; // pad size chars or lead size bytes + const sizage = Matter.Sizes.get(code); + + if (sizage!.fs === undefined) { + // Variable size code, NOT SUPPORTED + const cs = sizage!.hs + sizage!.ss; + if (cs % 4) { + throw new Error( + `Whole code size not multiple of 4 for variable length material. cs=${cs}` + ); + } + if (size < 0 || size > 64 ** sizage!.ss - 1) { + throw new Error(`Invalid size=${size} for code=${code}.`); + } + + const both = `${code}${intToB64(size, sizage!.ss)}`; + if (both.length % 4 !== ps - sizage!.ls!) { + throw new Error( + `Invalid code=${both} for converted raw pad size=${ps}.` + ); + } + + const bytes = new Uint8Array(sizage!.ls! + raw.length); + for (let i = 0; i < sizage!.ls!; i++) { + bytes[i] = 0; + } + for (let i = 0; i < raw.length; i++) { + const odx = i + ps; + bytes[odx] = raw[i]; + } + + return both + encodeBase64Url(bytes); + } else { + const both = code; + const cs = both.length; + if (cs % 4 != ps - sizage!.ls!) { + // adjusted pad given lead bytes + throw new Error( + `Invalid code=${both} for converted raw pad size=${ps}, ${raw.length}.` + ); + } + // prepad, convert, and replace upfront + // when fixed and ls != 0 then cs % 4 is zero and ps==ls + // otherwise fixed and ls == 0 then cs % 4 == ps + const bytes = new Uint8Array(ps + raw.length); + for (let i = 0; i < ps; i++) { + bytes[i] = 0; + } + for (let i = 0; i < raw.length; i++) { + const odx = i + ps; + bytes[odx] = raw[i]; + } + + return both + encodeBase64Url(bytes).slice(cs % 4); + } + } + + private _exfil(qb64: string) { + if (qb64.length == 0) { + throw new Error('Empty Material'); + } + + const first = qb64[0]; + if (!Array.from(Matter.Hards.keys()).includes(first)) { + throw new Error(`Unexpected code ${first}`); + } + + const hs = Matter.Hards.get(first); + if (qb64.length < hs!) { + throw new Error(`Shortage Error`); + } + + const hard = qb64.slice(0, hs); + if (!Array.from(Matter.Sizes.keys()).includes(hard)) { + throw new Error(`Unsupported code ${hard}`); + } + + const sizage = Matter.Sizes.get(hard); + const cs = sizage!.hs + sizage!.ss; + let size = -1; + if (sizage!.fs == -1) { + // Variable size code, Not supported + throw new Error('Variable size codes not supported yet'); + } else { + size = sizage!.fs!; + } + + if (qb64.length < sizage!.fs!) { + throw new Error(`Need ${sizage!.fs! - qb64.length} more chars.`); + } + + qb64 = qb64.slice(0, sizage!.fs); + const ps = cs % 4; + const pbs = 2 * (ps == 0 ? sizage!.ls! : ps); + let raw; + if (ps != 0) { + const base = new Array(ps + 1).join('A') + qb64.slice(cs); + const paw = Uint8Array.from(decodeBase64Url(base)); // decode base to leave prepadded raw + const pi = readInt(paw.subarray(0, ps)); // prepad as int + if (pi & (2 ** pbs - 1)) { + // masked pad bits non-zero + throw new Error( + `Non zeroed prepad bits = {pi & (2 ** pbs - 1 ):<06b} in {qb64b[cs:cs+1]}.` + ); + } + raw = paw.subarray(ps); // strip off ps prepad paw bytes + } else { + const base = qb64.slice(cs); + const paw = Uint8Array.from(decodeBase64Url(base)); + const li = readInt(paw.subarray(0, sizage!.ls)); + if (li != 0) { + if (li == 1) { + throw new Error(`Non zeroed lead byte = 0x{li:02x}.`); + } else { + throw new Error(`Non zeroed lead bytes = 0x{li:04x}`); + } + } + raw = paw.subarray(sizage!.ls); + } + + this._code = hard; // hard only + this._size = size; + this._raw = Uint8Array.from(raw); // ensure bytes so immutable and for crypto ops + } + + private _bexfil(qb2: Uint8Array) { + throw new Error(`qb2 not yet supported: ${qb2}`); + } +} diff --git a/packages/signify-ts/src/keri/core/number.ts b/packages/signify-ts/src/keri/core/number.ts new file mode 100644 index 00000000..e2365c69 --- /dev/null +++ b/packages/signify-ts/src/keri/core/number.ts @@ -0,0 +1,66 @@ +import { Matter, MatterArgs, NumDex } from './matter.ts'; +import { bytesToInt, intToBytes } from './utils.ts'; + +export class CesrNumber extends Matter { + constructor( + { raw, code, qb64b, qb64, qb2 }: MatterArgs, + num?: number | string, + numh?: string + ) { + let _num; + if ( + raw == undefined && + qb64 == undefined && + qb64b == undefined && + qb2 == undefined + ) { + if (typeof num == 'number') { + _num = num; + } else if (numh != undefined) { + _num = parseInt(numh, 16); + } else { + _num = 0; + } + } + + if (_num == undefined) { + throw new Error('Invalid whole number'); + } + + if (_num <= 256 ** 2 - 1) { + // make short version of code + code = NumDex.Short; + } else if (_num <= 256 ** 4 - 1) { + // make long version of code + code = code = NumDex.Long; + } else if (_num <= 256 ** 8 - 1) { + // make big version of code + code = code = NumDex.Big; + } else if (_num <= 256 ** 16 - 1) { + // make huge version of code + code = code = NumDex.Huge; + } else { + throw new Error(`Invalid num = ${num}, too large to encode.`); + } + + raw = intToBytes(_num, Matter._rawSize(code)); + + super({ raw, code, qb64b, qb64, qb2 }); + + if (!NumDex.has(this.code)) { + throw new Error('Invalid code ' + code + ' for Number'); + } + } + + get num(): number { + return bytesToInt(this.raw); + } + + get numh(): string { + return this.num.toString(16); + } + + get positive(): boolean { + return this.num > 0; + } +} diff --git a/packages/signify-ts/src/keri/core/pather.ts b/packages/signify-ts/src/keri/core/pather.ts new file mode 100644 index 00000000..7bd2d4e3 --- /dev/null +++ b/packages/signify-ts/src/keri/core/pather.ts @@ -0,0 +1,98 @@ +import { Bexter, Reb64 } from './bexter.ts'; +import { MatterArgs, MtrDex } from './matter.ts'; +import { EmptyMaterialError } from './kering.ts'; + +/* + Pather is a subclass of Bexter that provides SAD Path language specific functionality + for variable length strings that only contain Base64 URL safe characters. Pather allows + the specification of SAD Paths as a list of field components which will be converted to the + Base64 URL safe character representation. + + Additionally, Pather provides .rawify for extracting and serializing the content targeted by + .path for a SAD, represented as an instance of Serder. Pather enforces Base64 URL character + safety by leveraging the fact that SADs must have static field ordering. Any field label can + be replaced by its field ordinal to allow for path specification and traversal for any field + labels that contain non-Base64 URL safe characters. + + + Examples: strings: + path = [] + text = "-" + qb64 = '6AABAAA-' + + path = ["A"] + text = "-A" + qb64 = '5AABAA-A' + + path = ["A", "B"] + text = "-A-B" + qb64 = '4AAB-A-B' + + path = ["A", 1, "B", 3] + text = "-A-1-B-3" + qb64 = '4AAC-A-1-B-3' + + */ + +export class Pather extends Bexter { + constructor( + { raw, code = MtrDex.StrB64_L0, qb64b, qb64, qb2 }: MatterArgs, + bext?: string, + path?: string[] + ) { + if ( + raw === undefined && + bext === undefined && + qb64b === undefined && + qb64 === undefined && + qb2 === undefined + ) { + if (path === undefined) + throw new EmptyMaterialError('Missing bext string.'); + + bext = Pather._bextify(path); + } + + super({ raw, code, qb64b, qb64, qb2 }, bext); + } + + // TODO: implement SAD access methods like resolve, root, strip, startswith and tail + + get path(): string[] { + if (!this.bext.startsWith('-')) { + throw new Error('invalid SAD ptr'); + } + + let path = this.bext; + while (path.charAt(0) === '-') { + path = path.substring(1); + } + + const apath = path.split('-'); + if (apath[0] !== '') { + return apath; + } else { + return []; + } + } + + static _bextify(path: any[]): string { + const vath = []; + for (const p of path) { + let sp = ''; + if (typeof p === 'number') { + sp = p.toString(); + } else { + sp = p; + } + + const match = Reb64.exec(sp); + if (!match) { + throw new Error(`"Non Base64 path component = ${p}.`); + } + + vath.push(sp); + } + return '-' + vath.join('-'); + } +} diff --git a/packages/signify-ts/src/keri/core/prefixer.ts b/packages/signify-ts/src/keri/core/prefixer.ts new file mode 100644 index 00000000..27abd039 --- /dev/null +++ b/packages/signify-ts/src/keri/core/prefixer.ts @@ -0,0 +1,231 @@ +import { Matter, MatterArgs, MtrDex } from './matter.ts'; +import { EmptyMaterialError } from './kering.ts'; +import { Dict, Ilks } from './core.ts'; +import { sizeify } from './serder.ts'; +import { Verfer } from './verfer.ts'; +import { blake3 } from '@noble/hashes/blake3'; + +const Dummy: string = '#'; + +export class Prefixer extends Matter { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + private readonly _derive: Function | undefined; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + private readonly _verify: Function | undefined; + + constructor({ raw, code, qb64b, qb64, qb2 }: MatterArgs, ked?: Dict) { + try { + super({ raw, code, qb64b, qb64, qb2 }); + } catch (e) { + if (e instanceof EmptyMaterialError) { + if (ked == undefined || (code == undefined && !('i' in ked))) { + throw e; + } + + if (code == undefined) { + super({ qb64: ked['i'], code: code }); + code = this.code; + } + + let _derive; + if (code == MtrDex.Ed25519N) { + _derive = Prefixer._derive_ed25519N; + } else if (code == MtrDex.Ed25519) { + _derive = Prefixer._derive_ed25519; + } else if (code == MtrDex.Blake3_256) { + _derive = Prefixer._derive_blake3_256; + } else { + throw new Error(`Unsupported code = ${code} for prefixer.`); + } + + [raw, code] = _derive(ked); + super({ raw: raw, code: code }); + this._derive = _derive; + } else { + throw e; + } + } + + if (this.code == MtrDex.Ed25519N) { + this._verify = this._verify_ed25519N; + } else if (this.code == MtrDex.Ed25519) { + this._verify = this._verify_ed25519; + } else if (this.code == MtrDex.Blake3_256) { + this._verify = this._verify_blake3_256; + } else { + throw new Error(`Unsupported code = ${code} for prefixer.`); + } + } + + derive(sad: Dict): [Uint8Array, string] { + if (sad['i'] != Ilks.icp) { + throw new Error( + `Non-incepting ilk ${sad['i']} for prefix derivation` + ); + } + return this._derive!(sad); + } + + verify(sad: Dict, prefixed: boolean = false): boolean { + if (sad['i'] != Ilks.icp) { + throw new Error( + `Non-incepting ilk ${sad['i']} for prefix derivation` + ); + } + return this._verify!(sad, this.qb64, prefixed); + } + + static _derive_ed25519N(sad: Dict): [Uint8Array, string] { + let verfer; + const keys = sad['k']; + if (keys.length != 1) { + throw new Error( + `Basic derivation needs at most 1 key got ${keys.length} keys instead` + ); + } + try { + verfer = new Verfer({ qb64: keys[0] }); + } catch (e) { + throw new Error(`Error extracting public key = ${e}`); + } + + if (verfer.code != MtrDex.Ed25519N) { + throw new Error(`Mismatch derivation code = ${verfer.code}`); + } + + const next = 'n' in sad ? sad['n'] : []; + if (verfer.code == MtrDex.Ed25519N && next.length > 0) { + throw new Error( + `Non-empty nxt = ${next} for non-transferable code = ${verfer.code}` + ); + } + + const backers = 'b' in sad ? sad['b'] : []; + if (verfer.code == MtrDex.Ed25519N && backers.length > 0) { + throw new Error( + `Non-empty b =${backers} for non-transferable code = ${verfer.code}` + ); + } + + const anchor = 'a' in sad ? sad['a'] : []; + if (verfer.code == MtrDex.Ed25519N && anchor.length > 0) { + throw new Error( + `Non-empty a = ${verfer.code} for non-transferable code = ${verfer.code}` + ); + } + + return [verfer.raw, verfer.code]; + } + + static _derive_ed25519(sad: Dict): [Uint8Array, string] { + let verfer; + const keys = sad['k']; + if (keys.length != 1) { + throw new Error( + `Basic derivation needs at most 1 key got ${keys.length} keys instead` + ); + } + + try { + verfer = new Verfer({ qb64: keys[0] }); + } catch (e) { + throw new Error(`Error extracting public key = ${e}`); + } + + if (verfer.code in [MtrDex.Ed25519]) { + throw new Error(`Mismatch derivation code = ${verfer.code}`); + } + + return [verfer.raw, verfer.code]; + } + + static _derive_blake3_256(sad: Dict): [Uint8Array, string] { + const ilk = sad['t']; + if (![Ilks.icp, Ilks.dip, Ilks.vcp, Ilks.dip].includes(ilk)) { + throw new Error(`Invalid ilk = ${ilk} to derive pre.`); + } + + sad['i'] = ''.padStart(Matter.Sizes.get(MtrDex.Blake3_256)!.fs!, Dummy); + sad['d'] = sad['i']; + const [raw] = sizeify(sad); + const dig = blake3.create({ dkLen: 32 }).update(raw).digest(); + return [dig, MtrDex.Blake3_256]; + } + + _verify_ed25519N( + sad: Dict, + pre: string, + prefixed: boolean = false + ): boolean { + try { + const keys = sad['k']; + if (keys.length != 1) { + return false; + } + + if (keys[0] != pre) { + return false; + } + + if (prefixed && sad['i'] != pre) { + return false; + } + + const next = 'n' in sad ? sad['n'] : []; + if (next.length > 0) { + // must be empty + return false; + } + } catch (e) { + return false; + } + + return true; + } + + _verify_ed25519( + sad: Dict, + pre: string, + prefixed: boolean = false + ): boolean { + try { + const keys = sad['k']; + if (keys.length != 1) { + return false; + } + + if (keys[0] != pre) { + return false; + } + + if (prefixed && sad['i'] != pre) { + return false; + } + } catch (e) { + return false; + } + + return true; + } + + _verify_blake3_256( + sad: Dict, + pre: string, + prefixed: boolean = false + ): boolean { + try { + const [raw] = Prefixer._derive_blake3_256(sad); + const crymat = new Matter({ raw: raw, code: MtrDex.Blake3_256 }); + if (crymat.qb64 != pre) { + return false; + } + + if (prefixed && sad['i'] != pre) { + return false; + } + } catch (e) { + return false; + } + return true; + } +} diff --git a/packages/signify-ts/src/keri/core/saider.ts b/packages/signify-ts/src/keri/core/saider.ts new file mode 100644 index 00000000..479d6b5b --- /dev/null +++ b/packages/signify-ts/src/keri/core/saider.ts @@ -0,0 +1,154 @@ +import { DigiDex, Matter, MatterArgs, MtrDex } from './matter.ts'; +import { deversify, Dict, Serials } from './core.ts'; +import { EmptyMaterialError } from './kering.ts'; +import { dumps, sizeify } from './serder.ts'; +import { blake3 } from '@noble/hashes/blake3'; + +const Dummy = '#'; + +export enum Ids { + d = 'd', +} + +export class Saider extends Matter { + constructor( + { raw, code, qb64b, qb64, qb2 }: MatterArgs, + sad?: Dict, + kind?: Serials, + label: string = Ids.d + ) { + try { + super({ raw, code, qb64b, qb64, qb2 }); + } catch (e) { + if (e instanceof EmptyMaterialError) { + if (sad == undefined || !(label in sad)) { + throw e; + } + + if (code == undefined) { + if (sad[label] != '') { + super({ qb64: sad[label], code: code }); + code = this.code; + } else { + code = MtrDex.Blake3_256; + } + } + + if (!DigiDex.has(code)) { + throw new Error(`Unsupported digest code = ${code}`); + } + + [raw] = Saider._derive({ ...sad }, code, kind, label); + super({ raw: raw, code: code }); + } else { + throw e; + } + } + + if (!this.digestive) { + throw new Error(`Unsupported digest code = ${this.code}.`); + } + } + + private static _derive( + sad: Dict, + code: string, + kind: Serials | undefined, + label: string + ): [Uint8Array, Dict] { + if (!DigiDex.has(code)) { + throw new Error(`Unsupported digest code = ${code}.`); + } + + sad = { ...sad }; + sad[label] = ''.padStart(Matter.Sizes.get(code)!.fs!, Dummy); + if ('v' in sad) { + [, , kind, sad] = sizeify(sad, kind); + } + + const ser = { ...sad }; + + const cpa = Saider._serialze(ser, kind); + + switch (code) { + case MtrDex.Blake3_256: + return [blake3.create({ dkLen: 32 }).update(cpa).digest(), sad]; + default: + throw new Error(`Unsupported digest code = ${code}.`); + } + } + + public derive( + sad: Dict, + code: string, + kind: Serials | undefined, + label: string + ): [Uint8Array, Dict] { + code = code != undefined ? code : this.code; + return Saider._derive(sad, code, kind, label); + } + + public verify( + sad: Dict, + prefixed: boolean = false, + versioned: boolean = false, + kind?: Serials, + label: string = Ids.d + ): boolean { + try { + const [raw, dsad] = Saider._derive(sad, this.code, kind, label); + const saider = new Saider({ raw: raw, code: this.code }); + if (this.qb64 != saider.qb64) { + return false; + } + + if ('v' in sad && versioned) { + if (sad['v'] != dsad['v']) { + return false; + } + } + + if (prefixed && sad[label] != this.qb64) { + return false; + } + } catch (e) { + return false; + } + + return true; + } + + private static _serialze(sad: Dict, kind?: Serials): string { + let knd = Serials.JSON; + if ('v' in sad) { + [, knd] = deversify(sad['v']); + } + + if (kind == undefined) { + kind = knd; + } + + return dumps(sad, kind); + } + + public static saidify( + sad: Dict, + code: string = MtrDex.Blake3_256, + kind: Serials = Serials.JSON, + label: string = Ids.d + ): [Saider, Dict] { + if (!(label in sad)) { + throw new Error(`Missing id field labeled=${label} in sad.`); + } + let raw; + [raw, sad] = Saider._derive(sad, code, kind, label); + const saider = new Saider( + { raw: raw, code: code }, + undefined, + kind, + label + ); + sad[label] = saider.qb64; + return [saider, sad]; + } +} diff --git a/packages/signify-ts/src/keri/core/salter.ts b/packages/signify-ts/src/keri/core/salter.ts new file mode 100644 index 00000000..61fbe8d1 --- /dev/null +++ b/packages/signify-ts/src/keri/core/salter.ts @@ -0,0 +1,158 @@ +import { Signer } from './signer.ts'; + +import { Matter, MtrDex } from './matter.ts'; +import { EmptyMaterialError } from './kering.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Tier } from '../../types/keria-api-schema.ts'; + +export { Tier } from '../../types/keria-api-schema.ts'; + +interface SalterArgs { + raw?: Uint8Array | undefined; + code?: string; + tier?: Tier; + qb64b?: Uint8Array | undefined; + qb64?: string; + qb2?: Uint8Array | undefined; +} + +/** + * Maintains a random salt for secrets (private keys). + * Its .raw is random salt, .code as cipher suite for salt + */ +export class Salter extends Matter { + private readonly _tier: Tier | null; + + /** + * Creates a Salter from the provided raw salt bytes or generates a random salt if raw is not provided. + * Defaults to low security tier. Only supports Salt_128 salt type. + * @param salterArgs defines the kind of cryptographic seed to create with a variety of raw material initialization sources. + */ + constructor(salterArgs: SalterArgs) { + const { + raw, + code = MtrDex.Salt_128, + tier = Tier.low, + qb64, + qb64b, + qb2, + } = salterArgs; + try { + super({ raw, code, qb64, qb64b, qb2 }); + } catch (e) { + if (e instanceof EmptyMaterialError) { + if (code == MtrDex.Salt_128) { + const salt = libsodium.randombytes_buf( + libsodium.crypto_pwhash_SALTBYTES + ); + super({ raw: salt, code: code }); + } else { + throw new Error( + 'invalid code for Salter, only Salt_128 accepted' + ); + } + } else { + throw e; + } + } + + if (this.code != MtrDex.Salt_128) { + throw new Error('invalid code for Salter, only Salt_128 accepted'); + } + + this._tier = tier !== null ? tier : Tier.low; + } + + /** + * Stretches the salt to a secret key using the path, .raw, tier, and size determined by self.code. + * + * @param size number of bytes of the stretched seed + * @param path string of bytes prepended (prefixed) to the salt before stretching + * @param tier security tier for stretching + * @param temp boolean, True means use temporary, insecure tier; for testing only + * @returns stretched raw binary seed (secret) derived from path and .raw, and size using argon2d stretching algorithm. + * @private + */ + private stretch( + size: number = 32, + path: string = '', + tier: Tier | null = null, + temp: boolean = false + ): Uint8Array { + tier = tier == null ? this.tier : tier; + + let opslimit: number, memlimit: number; + + // Harcoded values based on keripy + if (temp) { + opslimit = 1; //libsodium.crypto_pwhash_OPSLIMIT_MIN + memlimit = 8192; //libsodium.crypto_pwhash_MEMLIMIT_MIN + } else { + switch (tier) { + case Tier.low: + opslimit = 2; //libsodium.crypto_pwhash_OPSLIMIT_INTERACTIVE + memlimit = 67108864; //libsodium.crypto_pwhash_MEMLIMIT_INTERACTIVE + break; + case Tier.med: + opslimit = 3; //libsodium.crypto_pwhash_OPSLIMIT_MODERATE + memlimit = 268435456; //libsodium.crypto_pwhash_MEMLIMIT_MODERATE + break; + case Tier.high: + opslimit = 4; //libsodium.crypto_pwhash_OPSLIMIT_SENSITIVE + memlimit = 1073741824; //libsodium.crypto_pwhash_MEMLIMIT_SENSITIVE + break; + default: + throw new Error(`Unsupported security tier = ${tier}.`); + } + } + + return libsodium.crypto_pwhash( + size, + path, + this.raw, + opslimit, + memlimit, + libsodium.crypto_pwhash_ALG_ARGON2ID13 + ); + } + + /** + * Returns Signer with the private key secret derived from code the path, the user entered passcode as a salt, + * and the security tier sized by the CESR cryptographic seed size indicated by the code. See the example below. + * The Signer's public key for its .verfer is derived from its private key, the Matter code, and the transferable boolean. + * + * The construction of the raw hash bytes used looks like this: + * ( size, password, salt ) + * where + * ( code size, path, Base64Decode(passcode) ) + * for example, for the initial inception signing key the following parameters are used: + * ( 32, "signify:controller00", Base64Decode("Athisismysecretkeyseed") ) + * and for the initial rotation key pair the following parameters are used: + * ( 32, "signify:controller01", Base64Decode("Athisismysecretkeyseed") ) + * + * @param code derivation code indicating seed type + * @param transferable whether or not the key is for a transferable or non-transferable identifier. + * @param path string of bytes prepended (prefixed) to the salt before stretching + * @param tier security tier for stretching + * @param temp boolean, True means use temporary, insecure tier; for testing only + */ + signer( + code: string = MtrDex.Ed25519_Seed, + transferable: boolean = true, + path: string = '', + tier: Tier | null = null, + temp: boolean = false + ): Signer { + const seed = this.stretch(Matter._rawSize(code), path, tier, temp); + + return new Signer({ + raw: seed, // private key + code: code, + transferable: transferable, + }); + } + + get tier(): Tier | null { + return this._tier; + } +} diff --git a/packages/signify-ts/src/keri/core/seqner.ts b/packages/signify-ts/src/keri/core/seqner.ts new file mode 100644 index 00000000..531134f2 --- /dev/null +++ b/packages/signify-ts/src/keri/core/seqner.ts @@ -0,0 +1,46 @@ +import { Matter, MatterArgs, MtrDex } from './matter.ts'; +import { intToBytes, bytesToInt } from './utils.ts'; +/** + * @description Seqner: subclass of Matter, cryptographic material, for ordinal numbers + * such as sequence numbers or first seen ordering numbers. + * Seqner provides fully qualified format for ordinals (sequence numbers etc) + * when provided as attached cryptographic material elements. + */ +export class Seqner extends Matter { + constructor({ + raw, + code = MtrDex.Salt_128, + qb64, + qb64b, + qb2, + sn, + snh, + ...kwa + }: MatterArgs & { sn?: number; snh?: string }) { + if (!raw && !qb64b && !qb64 && !qb2) { + if (sn === undefined) { + if (snh === undefined) { + sn = 0; + } else { + sn = parseInt(snh, 16); + } + } + + raw = intToBytes(sn, Matter._rawSize(MtrDex.Salt_128)); + } + + super({ raw, code, qb64, qb64b, qb2, ...kwa }); + + if (this.code !== MtrDex.Salt_128) { + throw new Error(`Invalid code = ${this.code} for Seqner.`); + } + } + + get sn(): number { + return bytesToInt(this.raw); //To check if other readUInt64 is needed + } + + get snh(): string { + return this.sn.toString(16); + } +} diff --git a/packages/signify-ts/src/keri/core/serder.ts b/packages/signify-ts/src/keri/core/serder.ts new file mode 100644 index 00000000..d5995aa5 --- /dev/null +++ b/packages/signify-ts/src/keri/core/serder.ts @@ -0,0 +1,174 @@ +import { MtrDex } from './matter.ts'; +import { + deversify, + Dict, + Protocols, + Serials, + versify, + Version, + Vrsn_1_0, +} from './core.ts'; +import { Verfer } from './verfer.ts'; +import { Diger } from './diger.ts'; +import { CesrNumber } from './number.ts'; + +export class Serder { + private _kind: Serials; + private _raw: string = ''; + private _sad: Dict = {}; + private _proto: Protocols = Protocols.KERI; + private _size: number = 0; + private _version: Version = Vrsn_1_0; + private readonly _code: string; + + /** + * Creates a new Serder object from a self-addressing data dictionary. + * @param sad self-addressing data dictionary. + * @param kind serialization type to produce + * @param code derivation code for the prefix + */ + constructor( + sad: Dict, + kind: Serials = Serials.JSON, + code: string = MtrDex.Blake3_256 + ) { + const [raw, proto, eKind, eSad, version] = this._exhale(sad, kind); + this._raw = raw; + this._sad = eSad; + this._proto = proto; + this._version = version; + this._code = code; + this._kind = eKind; + this._size = raw.length; + } + + get sad(): Dict { + return this._sad; + } + + get pre(): string { + return this._sad['i']; + } + + get code(): string { + return this._code; + } + + get raw(): string { + return this._raw; + } + + get said(): string { + return this._sad['d']; + } + + get sner(): CesrNumber { + return new CesrNumber({}, this.sad['s']); + } + + get sn(): number { + return this.sner.num; + } + + get kind(): Serials { + return this._kind; + } + + /** + * Serializes a self-addressing data dictionary from the dictionary passed in + * using the specified serialization type. + * @param sad self-addressing data dictionary. + * @param kind serialization type to produce + * @private + */ + private _exhale( + sad: Dict, + kind: Serials + ): [string, Protocols, Serials, Dict, Version] { + return sizeify(sad, kind); + } + + get proto(): Protocols { + return this._proto; + } + + get size(): number { + return this._size; + } + + get version(): Version { + return this._version; + } + get verfers(): Verfer[] { + let keys: any = []; + if ('k' in this._sad) { + // establishment event + keys = this._sad['k']; + } else { + // non-establishment event + keys = []; + } + // create a new Verfer for each key + const verfers = []; + for (const key of keys) { + verfers.push(new Verfer({ qb64: key })); + } + return verfers; + } + + get digers(): Diger[] { + let keys: any = []; + if ('n' in this._sad) { + // establishment event + keys = this._sad['n']; + } else { + // non-establishment event + keys = []; + } + // create a new Verfer for each key + const digers = []; + for (const key of keys) { + digers.push(new Diger({ qb64: key })); + } + return digers; + } + + pretty() { + return JSON.stringify(this._sad, undefined, 2); + } +} + +export function dumps(sad: object, kind: Serials.JSON): string { + if (kind == Serials.JSON) { + return JSON.stringify(sad); + } else { + throw new Error('unsupported event encoding'); + } +} + +export function sizeify( + ked: Dict, + kind?: Serials +): [string, Protocols, Serials, Dict, Version] { + if (!('v' in ked)) { + throw new Error('Missing or empty version string'); + } + + const [proto, knd, version] = deversify(ked['v'] as string); + if (version != Vrsn_1_0) { + throw new Error(`unsupported version ${version.toString()}`); + } + + if (kind == undefined) { + kind = knd; + } + + let raw = dumps(ked, kind); + const size = new TextEncoder().encode(raw).length; + + ked['v'] = versify(proto, version, kind, size); + + raw = dumps(ked, kind); + + return [raw, proto, kind, ked, version]; +} diff --git a/packages/signify-ts/src/keri/core/siger.ts b/packages/signify-ts/src/keri/core/siger.ts new file mode 100644 index 00000000..8caa88a5 --- /dev/null +++ b/packages/signify-ts/src/keri/core/siger.ts @@ -0,0 +1,40 @@ +import { IdxSigDex, Indexer, IndexerArgs } from './indexer.ts'; +import { Verfer } from './verfer.ts'; + +/** + Siger is subclass of Indexer, indexed signature material, + Adds .verfer property which is instance of Verfer that provides + associated signature verifier. + + See Indexer for inherited attributes and properties: + + Attributes: + + Properties: + .verfer is Verfer object instance + + Methods: + **/ + +export class Siger extends Indexer { + private _verfer?: Verfer; + constructor( + { raw, code, index, ondex, qb64, qb64b, qb2 }: IndexerArgs, + verfer?: Verfer + ) { + super({ raw, code, index, ondex, qb64, qb64b, qb2 }); + + if (!IdxSigDex.has(this.code)) { + throw new Error(`Invalid code = ${this.code} for Siger.`); + } + this._verfer = verfer; + } + + get verfer(): Verfer | undefined { + return this._verfer; + } + + set verfer(verfer: Verfer | undefined) { + this._verfer = verfer; + } +} diff --git a/packages/signify-ts/src/keri/core/signer.ts b/packages/signify-ts/src/keri/core/signer.ts new file mode 100644 index 00000000..95f30453 --- /dev/null +++ b/packages/signify-ts/src/keri/core/signer.ts @@ -0,0 +1,134 @@ +import { EmptyMaterialError } from './kering.ts'; + +export {}; +import libsodium from 'libsodium-wrappers-sumo'; +import { Matter } from './matter.ts'; +import { MtrDex } from './matter.ts'; +import { Verfer } from './verfer.ts'; +import { Cigar } from './cigar.ts'; +import { Siger } from './siger.ts'; +import { IdrDex } from './indexer.ts'; +import { concat } from './core.ts'; + +/** + * @description Signer is Matter subclass with method to create signature of serialization + * It will use .raw as signing (private) key seed + * .code as cipher suite for signing and new property .verfer whose property + * .raw is public key for signing. + * If not provided .verfer is generated from private key seed using .code + as cipher suite for creating key-pair. + */ +interface SignerArgs { + raw?: Uint8Array | undefined; + code?: string; + qb64b?: Uint8Array | undefined; + qb64?: string; + qb2?: Uint8Array | undefined; + transferable?: boolean; +} + +export class Signer extends Matter { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + private readonly _sign: Function; + private readonly _verfer: Verfer; + + constructor({ + raw, + code = MtrDex.Ed25519_Seed, + qb64, + qb64b, + qb2, + transferable = true, + }: SignerArgs) { + try { + super({ raw, code, qb64, qb64b, qb2 }); + } catch (e) { + if (e instanceof EmptyMaterialError) { + if (code == MtrDex.Ed25519_Seed) { + const raw = libsodium.randombytes_buf( + libsodium.crypto_sign_SEEDBYTES + ); + super({ raw, code, qb64, qb64b, qb2 }); + } else { + throw new Error(`Unsupported signer code = ${code}.`); + } + } else { + throw e; + } + } + let verfer; + if (this.code == MtrDex.Ed25519_Seed) { + this._sign = this._ed25519; + const keypair = libsodium.crypto_sign_seed_keypair(this.raw); + verfer = new Verfer({ + raw: keypair.publicKey, + code: transferable ? MtrDex.Ed25519 : MtrDex.Ed25519N, + }); + } else { + throw new Error(`Unsupported signer code = ${this.code}.`); + } + + this._verfer = verfer; + } + + /** + * @description Property verfer: + Returns Verfer instance + Assumes ._verfer is correctly assigned + */ + get verfer(): Verfer { + return this._verfer; + } + + sign( + ser: Uint8Array, + index: number | null = null, + only: boolean = false, + ondex: number | undefined = undefined + ): Siger | Cigar { + return this._sign(ser, this.raw, this.verfer, index, only, ondex); + } + + _ed25519( + ser: Uint8Array, + seed: Uint8Array, + verfer: Verfer, + index: number | null, + only: boolean = false, + ondex: number | undefined + ) { + const sig = libsodium.crypto_sign_detached( + ser, + concat(seed, verfer.raw) + ); + + if (index == null) { + return new Cigar({ raw: sig, code: MtrDex.Ed25519_Sig }, verfer); + } else { + let code; + if (only) { + ondex = undefined; + if (index <= 63) { + code = IdrDex.Ed25519_Crt_Sig; + } else { + code = IdrDex.Ed25519_Big_Crt_Sig; + } + } else { + if (ondex == undefined) { + ondex = index; + } + + if (ondex == index && index <= 63) + // both same and small + code = IdrDex.Ed25519_Sig; // use small both same + // otherwise big or both not same so use big both + else code = IdrDex.Ed25519_Big_Sig; // use use big both + } + + return new Siger( + { raw: sig, code: code, index: index, ondex: ondex }, + verfer + ); + } + } +} diff --git a/packages/signify-ts/src/keri/core/tholder.ts b/packages/signify-ts/src/keri/core/tholder.ts new file mode 100644 index 00000000..745ea915 --- /dev/null +++ b/packages/signify-ts/src/keri/core/tholder.ts @@ -0,0 +1,217 @@ +import { BexDex, Matter, NumDex } from './matter.ts'; +import { CesrNumber } from './number.ts'; +import { Fraction, format, sum, fraction } from 'mathjs'; + +export class Tholder { + private _weighted: boolean = false; + private _thold: any = undefined; + private _size: number = 0; + private _number: CesrNumber | undefined = undefined; + private _satisfy: any = undefined; + + // private _bexter: any + + constructor(kargs: { thold?: any; limen?: any; sith?: any }) { + if (kargs.thold !== undefined) { + this._processThold(kargs.thold); + } else if (kargs.limen != undefined) { + this._processLimen(kargs.limen); + } else if (kargs.sith !== undefined) { + this._processSith(kargs.sith); + } else { + throw new Error('Missing threshold expression'); + } + } + + get weighted(): boolean { + return this._weighted; + } + + get thold(): any { + return this._thold; + } + + get size(): number { + return this._size; + } + + get limen(): any { + return this._number?.qb64b; + } + + get sith(): string { + if (this.weighted) { + let sith = this.thold.map((clause: Fraction[]) => { + return clause.map((c) => { + if (0 < Number(c) && Number(c) < 1) { + return format(c, { fraction: 'ratio' }); + } else { + return format(c, { fraction: 'decimal' }); + } + }); + }); + if (sith.length == 1) { + sith = sith[0]; + } + + return sith; + } else { + return this.thold.toString(16); + } + } + + get json(): string { + return JSON.stringify(this.sith); + } + + get num(): number | undefined { + return this._weighted ? undefined : this._thold; + } + + private _processThold(thold: number | Array>) { + if (typeof thold === 'number') { + this._processUnweighted(thold); + } else { + this._processWeighted(thold); + } + } + + private _processLimen(limen: string) { + const matter = new Matter({ qb64: limen }); + if (NumDex.has(matter.code)) { + const number = new CesrNumber({ + raw: matter.raw, + code: matter.code, + }); + this._processUnweighted(number.num); + } else if (BexDex.has(matter.code)) { + // TODO: Implement Bexter + } else { + throw new Error('Invalid code for limen=' + matter.code); + } + } + + private _processSith(sith: string | number | Array) { + if (typeof sith == 'number') { + this._processUnweighted(sith); + } else if (typeof sith == 'string' && sith.indexOf('[') == -1) { + this._processUnweighted(parseInt(sith, 16)); + } else { + let _sith: any = sith; + if (typeof sith == 'string') { + _sith = JSON.parse(sith); + } + + if (_sith.length == 0) { + throw new Error('Empty weight list'); + } + + const mask = _sith.map((x: any) => { + return typeof x !== 'string'; + }); + + if (mask.length > 0 && !mask.every((x: boolean) => x)) { + _sith = [_sith]; + } + + for (const c of _sith) { + const mask = c.map((x: any) => { + return typeof x === 'string'; + }); + if (mask.length > 0 && !mask.every((x: boolean) => x)) { + throw new Error( + 'Invalid sith, some weights in clause ' + + mask + + ' are non string' + ); + } + } + + const thold = this._processClauses(_sith); + this._processWeighted(thold); + } + } + + private _processClauses(sith: Array>): Fraction[][] { + const thold = new Array>(); + sith.forEach((clause) => { + thold.push( + clause.map((w) => { + return this.weight(w); + }) + ); + }); + return thold; + } + + private _processUnweighted(thold: number) { + if (thold < 0) { + throw new Error('Non-positive int threshold = {thold}.'); + } + this._thold = thold; + this._weighted = false; + this._size = this._thold; // used to verify that keys list size is at least size + this._satisfy = this._satisfy_numeric; + this._number = new CesrNumber({}, thold); + // this._bexter = undefined + } + + private _processWeighted(thold: Array>) { + for (const clause of thold) { + if (Number(sum(clause)) < 1) { + throw new Error( + 'Invalid sith clause: ' + + thold + + 'all clause weight sums must be >= 1' + ); + } + } + + this._thold = thold; + this._weighted = true; + this._size = thold.reduce((acc, currentValue) => { + return acc + currentValue.length; + }, 0); + this._satisfy = this._satisfy_weighted; + //TODO: created Bexter if needed + } + + private weight(w: string): Fraction { + return fraction(w); + } + + private _satisfy_numeric(indices: any[]) { + return this.thold > 0 && indices.length >= this.thold; // at least one + } + + private _satisfy_weighted(indices: any) { + if (indices.length === 0) { + return false; + } + + const indexes: Set = new Set(indices.sort()); + const sats = new Array(indices.length).fill(false); + for (const idx of indexes) { + sats[idx] = true; + } + let wio = 0; + for (const clause of this.thold) { + let cw = 0; + for (const w of clause) { + if (sats[wio]) { + cw += Number(w); + } + wio += 1; + } + if (cw < 1) { + return false; + } + } + + return true; + } + + public satisfy(indices: any): boolean { + return this._satisfy(indices); + } +} diff --git a/packages/signify-ts/src/keri/core/utils.ts b/packages/signify-ts/src/keri/core/utils.ts new file mode 100644 index 00000000..6e1cf980 --- /dev/null +++ b/packages/signify-ts/src/keri/core/utils.ts @@ -0,0 +1,169 @@ +import { Counter, CtrDex } from './counter.ts'; +import { Seqner } from './seqner.ts'; +import { Prefixer } from './prefixer.ts'; +import { Saider } from './saider.ts'; +import { Serder } from './serder.ts'; +import { b } from './core.ts'; + +export function pad(n: any, width = 3, z = 0) { + return (String(z).repeat(width) + String(n)).slice(String(n).length); +} + +/** + * @description Returns list of depth first recursively extracted values from elements of + key event dict ked whose flabels are in lables list + + * @param {*} ked ked is key event dict + * @param {*} labels labels is list of element labels in ked from which to extract values + */ +export function extractValues(ked: any, labels: any) { + let values = []; + for (const label of labels) { + values = extractElementValues(ked[label], values); + } + + return values; +} + +export function arrayEquals(ar1: Uint8Array, ar2: Uint8Array) { + return ( + ar1.length === ar2.length && + ar1.every((val, index) => val === ar2[index]) + ); +} + +/** + * @description Recusive depth first search that recursively extracts value(s) from element + and appends to values list + Assumes that extracted values are str + + * @param {*} element + * @param {*} values + */ + +function extractElementValues(element: any, values: any) { + let data = []; + + try { + if (element instanceof Array && !(typeof element == 'string')) { + for (const k in element) extractElementValues(element[k], values); + } else if (typeof element == 'string') { + values.push(element); + } + data = values; + } catch (error) { + throw new Error(error as string); + } + + return data; +} + +/** + * @description Returns True if obj is non-string iterable, False otherwise + + * @param {*} obj + */ + +// function nonStringIterable(obj) { +// obj instanceof (String) +// return instanceof(obj, (str, bytes)) && instanceof(obj, Iterable)) +// } + +export function nowUTC(): Date { + return new Date(); +} + +export function range(start: number, stop: number, step: number) { + if (typeof stop == 'undefined') { + // one param defined + stop = start; + start = 0; + } + + if (typeof step == 'undefined') { + step = 1; + } + + if ((step > 0 && start >= stop) || (step < 0 && start <= stop)) { + return []; + } + + const result = new Array(); + for (let i: number = start; step > 0 ? i < stop : i > stop; i += step) { + result.push(i); + } + + return result; +} + +export function intToBytes(value: number, length: number): Uint8Array { + const byteArray = new Uint8Array(length); // Assuming a 4-byte integer (32 bits) + + for (let index = byteArray.length - 1; index >= 0; index--) { + const byte = value & 0xff; + byteArray[index] = byte; + value = (value - byte) / 256; + } + return byteArray; +} + +export function bytesToInt(ar: Uint8Array): number { + let value = 0; + for (let i = 0; i < ar.length; i++) { + value = value * 256 + ar[i]; + } + return value; +} + +export function serializeACDCAttachment(anc: Serder): Uint8Array { + const prefixer = new Prefixer({ qb64: anc.pre }); + const seqner = new Seqner({ sn: anc.sn }); + const saider = new Saider({ qb64: anc.sad['d'] }); + const craw = new Uint8Array(); + const ctr = new Counter({ code: CtrDex.SealSourceTriples, count: 1 }).qb64b; + const prefix = prefixer.qb64b; + const seq = seqner.qb64b; + const said = saider.qb64b; + const newCraw = new Uint8Array( + craw.length + ctr.length + prefix.length + seq.length + said.length + ); + newCraw.set(craw); + newCraw.set(ctr, craw.length); + newCraw.set(prefix, craw.length + ctr.length); + newCraw.set(seq, craw.length + ctr.length + prefix.length); + newCraw.set(said, craw.length + ctr.length + prefix.length + seq.length); + return newCraw; +} + +export function serializeIssExnAttachment(anc: Serder): Uint8Array { + const seqner = new Seqner({ sn: anc.sn }); + const ancSaider = new Saider({ qb64: anc.sad['d'] }); + const coupleArray = new Uint8Array( + seqner.qb64b.length + ancSaider.qb64b.length + ); + coupleArray.set(seqner.qb64b); + coupleArray.set(ancSaider.qb64b, seqner.qb64b.length); + const counter = new Counter({ + code: CtrDex.SealSourceCouples, + count: 1, + }); + const counterQb64b = counter.qb64b; + const atc = new Uint8Array(counter.qb64b.length + coupleArray.length); + atc.set(counterQb64b); + atc.set(coupleArray, counterQb64b.length); + + if (atc.length % 4 !== 0) { + throw new Error( + `Invalid attachments size: ${atc.length}, non-integral quadlets detected.` + ); + } + const pcnt = new Counter({ + code: CtrDex.AttachedMaterialQuadlets, + count: Math.floor(atc.length / 4), + }); + const msg = new Uint8Array(pcnt.qb64b.length + atc.length); + msg.set(pcnt.qb64b); + msg.set(atc, pcnt.qb64b.length); + + return msg; +} diff --git a/packages/signify-ts/src/keri/core/vdring.ts b/packages/signify-ts/src/keri/core/vdring.ts new file mode 100644 index 00000000..8ce5836a --- /dev/null +++ b/packages/signify-ts/src/keri/core/vdring.ts @@ -0,0 +1,94 @@ +import { randomNonce } from '../app/coring.ts'; +import { TraitDex } from '../app/habery.ts'; +import { + Serials, + Vrsn_1_0, + Version, + Protocols, + versify, + Ilks, +} from '../core/core.ts'; +import { ample } from './eventing.ts'; +import { MtrDex } from './matter.ts'; +import { Prefixer } from './prefixer.ts'; +import { Serder } from './serder.ts'; + +namespace vdr { + export interface VDRInceptArgs { + pre: string; + toad?: number | string; + nonce?: string; + baks?: string[]; + cnfg?: string[]; + version?: Version; + kind?: Serials; + code?: string; + } + + export function incept({ + pre, + toad, + nonce = randomNonce(), + baks = [], + cnfg = [], + version = Vrsn_1_0, + kind = Serials.JSON, + code = MtrDex.Blake3_256, + }: VDRInceptArgs): Serder { + const vs = versify(Protocols.KERI, version, kind, 0); + const isn = 0; + const ilk = Ilks.vcp; + + if (cnfg.includes(TraitDex.NoBackers) && baks.length > 0) { + throw new Error( + `${baks.length} backers specified for NB vcp, 0 allowed` + ); + } + + if (new Set(baks).size < baks.length) { + throw new Error(`Invalid baks ${baks} has duplicates`); + } + + let _toad: number; + if (toad === undefined) { + if (baks.length === 0) { + _toad = 0; + } else { + _toad = ample(baks.length); + } + } else { + _toad = +toad; + } + + if (baks.length > 0) { + if (_toad < 1 || _toad > baks.length) { + throw new Error(`Invalid toad ${_toad} for baks in ${baks}`); + } + } else { + if (_toad != 0) { + throw new Error(`Invalid toad ${_toad} for no baks`); + } + } + + const sad = { + v: vs, + t: ilk, + d: '', + i: '', + ii: pre, + s: '' + isn, + c: cnfg, + bt: _toad.toString(16), + b: baks, + n: nonce, + }; + + const prefixer = new Prefixer({ code }, sad); + sad.i = prefixer.qb64; + sad.d = prefixer.qb64; + + return new Serder(sad); + } +} + +export { vdr }; diff --git a/packages/signify-ts/src/keri/core/verfer.ts b/packages/signify-ts/src/keri/core/verfer.ts new file mode 100644 index 00000000..8d48f62a --- /dev/null +++ b/packages/signify-ts/src/keri/core/verfer.ts @@ -0,0 +1,47 @@ +import libsodium from 'libsodium-wrappers-sumo'; +import { Matter, MatterArgs, MtrDex } from './matter.ts'; +import { p256 } from '@noble/curves/p256'; +import { b } from './core.ts'; + +const VERFER_CODES = new Set([ + MtrDex.Ed25519N, + MtrDex.Ed25519, + MtrDex.ECDSA_256r1N, + MtrDex.ECDSA_256r1, +]); + +/** + * @description Verfer :sublclass of Matter,helps to verify signature of serialization + * using .raw as verifier key and .code as signature cypher suite + */ +export class Verfer extends Matter { + constructor({ raw, code, qb64, qb64b, qb2 }: MatterArgs) { + super({ raw, code, qb64, qb64b, qb2 }); + + if (!VERFER_CODES.has(this.code)) { + throw new Error(`Unsupported code = ${this.code} for verifier.`); + } + } + + verify(sig: Uint8Array, ser: Uint8Array | string): boolean { + switch (this.code) { + case MtrDex.Ed25519: + case MtrDex.Ed25519N: { + return libsodium.crypto_sign_verify_detached( + sig, + ser, + this.raw + ); + } + case MtrDex.ECDSA_256r1: + case MtrDex.ECDSA_256r1N: { + const message = typeof ser === 'string' ? b(ser) : ser; + return p256.verify(sig, message, this.raw); + } + default: + throw new Error( + `Unsupported code = ${this.code} for verifier.` + ); + } + } +} diff --git a/packages/signify-ts/src/keri/end/ending.ts b/packages/signify-ts/src/keri/end/ending.ts new file mode 100644 index 00000000..d767db4e --- /dev/null +++ b/packages/signify-ts/src/keri/end/ending.ts @@ -0,0 +1,167 @@ +import { Siger } from '../core/siger.ts'; +import { Cigar } from '../core/cigar.ts'; + +export const FALSY = [false, 0, '?0', 'no', 'false', 'False', 'off']; +export const TRUTHY = [true, 1, '?1', 'yes', 'true', 'True', 'on']; + +export class Signage { + constructor( + public readonly markers: + | (Siger | Cigar)[] + | Map, + public readonly indexed?: boolean, + public readonly signer?: string, + public readonly ordinal?: string, + public readonly digest?: string, + public readonly kind?: string + ) {} +} + +export function signature(signages: Signage[]): Headers { + const values: string[] = []; + + for (const signage of signages) { + let markers: Array; + let tags: string[]; + + if (signage.markers instanceof Map) { + markers = Array.from(signage.markers.values()); + tags = Array.from(signage.markers.keys()); + } else { + markers = signage.markers as Array; + tags = []; + } + + const items = new Array(); + const indexed = signage.indexed ?? markers[0] instanceof Siger; + + if (indexed) { + items.push('indexed="?1"'); + } else { + items.push('indexed="?0"'); + } + + if (signage.signer != undefined) { + items.push(`signer="${signage.signer}"`); + } + if (signage.ordinal != undefined) { + items.push(`ordinal="${signage.ordinal}"`); + } + if (signage.digest != undefined) { + items.push(`digest="${signage.digest}"`); + } + if (signage.kind != undefined) { + items.push(`kind="${signage.kind}"`); + } + + items.push( + ...markers.map((marker, idx) => { + let tag: string | undefined = undefined; + let val: string; + + if (tags != undefined && tags.length > idx) { + tag = tags[idx]; + } + + if (marker instanceof Siger) { + if (!indexed) { + throw new Error( + `Indexed signature marker ${marker} when indexed False.` + ); + } + + tag = tag ?? marker.index.toString(); + val = marker.qb64; + } else if (marker instanceof Cigar) { + if (indexed) { + throw new Error( + `Unindexed signature marker ${marker} when indexed True.` + ); + } + if (!marker.verfer) { + throw new Error( + `Indexed signature marker is missing verfer` + ); + } + + tag = tag ?? marker.verfer.qb64; + val = marker.qb64; + } else { + tag = tag ?? idx.toString(); + val = marker; + } + + return `${tag}="${val}"`; + }) + ); + + values.push(items.join(';')); + } + + return new Headers([['Signature', values.join(',')]]); +} + +export function designature(value: string) { + const values = value.replace(' ', '').split(','); + + const signages = values.map((val) => { + const dict = new Map(); + val.split(';').forEach((v) => { + const splits = v.split('=', 2); + dict.set(splits[0], splits[1].replaceAll('"', '')); + }); + + if (!dict.has('indexed')) { + throw new Error( + 'Missing indexed field in Signature header signage.' + ); + } + const item = dict.get('indexed')!; + const indexed = !FALSY.includes(item); + dict.delete('indexed'); + + let signer; + if (dict.has('signer')) { + signer = dict.get('signer') as string; + dict.delete('signer'); + } + + let ordinal; + if (dict.has('ordinal')) { + ordinal = dict.get('ordinal') as string; + dict.delete('ordinal'); + } + + let digest; + if (dict.has('digest')) { + digest = dict.get('digest') as string; + dict.delete('digest'); + } + + let kind; + if (dict.has('kind')) { + kind = dict.get('kind') as string; + dict.delete('kind'); + } else { + kind = 'CESR'; + } + + if (kind == 'CESR') { + const markers = new Map(); + + for (const [key, val] of dict.entries()) { + if (indexed) { + markers.set(key, new Siger({ qb64: val })); + } else { + markers.set(key, new Cigar({ qb64: val })); + } + } + + return new Signage(markers, indexed, signer, ordinal, digest, kind); + } else { + return new Signage(dict, indexed, signer, ordinal, digest, kind); + } + }); + + return signages; +} diff --git a/packages/signify-ts/src/ready.ts b/packages/signify-ts/src/ready.ts new file mode 100644 index 00000000..23585f26 --- /dev/null +++ b/packages/signify-ts/src/ready.ts @@ -0,0 +1,5 @@ +import _sodium from 'libsodium-wrappers-sumo'; + +export const ready: () => Promise = async () => { + await _sodium.ready; +}; diff --git a/packages/signify-ts/src/types/keria-api-schema.ts b/packages/signify-ts/src/types/keria-api-schema.ts new file mode 100644 index 00000000..5360b47d --- /dev/null +++ b/packages/signify-ts/src/types/keria-api-schema.ts @@ -0,0 +1,577 @@ +// AUTO-GENERATED: Only components retained from OpenAPI schema + +export interface components { + schemas: { + ACDCAttributes: { + dt?: string; + i?: string; + u?: string; + } & { + [key: string]: unknown; + }; + ACDC_V_1: + | { + v: string; + d: string; + i: string; + s: string; + u?: string; + ri?: string; + e?: string; + r?: string; + a?: components['schemas']['ACDCAttributes']; + } + | { + v: string; + d: string; + i: string; + s: string; + u?: string; + ri?: string; + e?: string; + r?: string; + A?: string | unknown[]; + }; + ACDC_V_2: + | { + v: string; + d: string; + i: string; + s: string; + u?: string; + rd?: string; + e?: string; + r?: string; + a?: components['schemas']['ACDCAttributes']; + } + | { + v: string; + d: string; + i: string; + s: string; + u?: string; + rd?: string; + e?: string; + r?: string; + A?: string | unknown[]; + }; + IssEvent: { + v: string; + /** @enum {unknown} */ + t: IssEventT; + d: string; + i: string; + s: string; + ri: string; + dt: string; + }; + Schema: { + $id: string; + $schema: string; + title: string; + description: string; + type: string; + credentialType: string; + version: string; + properties: { + [key: string]: unknown; + }; + additionalProperties: boolean; + required: string[]; + }; + Anchor: { + pre: string; + sn: number; + d: string; + }; + Seal: { + s: string; + d: string; + i?: string; + }; + IXN_V_1: { + v: string; + t: string; + d: string; + i: string; + s: string; + p: string; + a: components['schemas']['Seal'][]; + }; + IXN_V_2: { + v: string; + t: string; + d: string; + i: string; + s: string; + p: string; + a: components['schemas']['Seal'][]; + }; + ICP_V_1: { + v: string; + t: string; + d: string; + i: string; + s: string; + kt: string | string[] | string[][]; + k: string[]; + nt: string | string[] | string[][]; + n: string[]; + bt: string; + b: string[]; + c: string[]; + a: unknown; + }; + ICP_V_2: { + v: string; + t: string; + d: string; + i: string; + s: string; + kt: string | string[] | string[][]; + k: string[]; + nt: string | string[] | string[][]; + n: string[]; + bt: string; + b: string[]; + c: string[]; + a: unknown; + }; + ROT_V_1: { + v: string; + t: string; + d: string; + i: string; + s: string; + p: string; + kt: string | string[] | string[][]; + k: string[]; + nt: string | string[] | string[][]; + n: string[]; + bt: string; + br: string[]; + ba: string[]; + a: unknown; + }; + ROT_V_2: { + v: string; + t: string; + d: string; + i: string; + s: string; + p: string; + kt: string | string[] | string[][]; + k: string[]; + nt: string | string[] | string[][]; + n: string[]; + bt: string; + br: string[]; + ba: string[]; + c: string[]; + a: unknown; + }; + DIP_V_1: { + v: string; + t: string; + d: string; + i: string; + s: string; + kt: string | string[] | string[][]; + k: string[]; + nt: string | string[] | string[][]; + n: string[]; + bt: string; + b: string[]; + c: string[]; + a: unknown; + di: string; + }; + DIP_V_2: { + v: string; + t: string; + d: string; + i: string; + s: string; + kt: string | string[] | string[][]; + k: string[]; + nt: string | string[] | string[][]; + n: string[]; + bt: string; + b: string[]; + c: string[]; + a: unknown; + di: string; + }; + DRT_V_1: { + v: string; + t: string; + d: string; + i: string; + s: string; + p: string; + kt: string | string[] | string[][]; + k: string[]; + nt: string | string[] | string[][]; + n: string[]; + bt: string; + br: string[]; + ba: string[]; + a: unknown; + }; + DRT_V_2: { + v: string; + t: string; + d: string; + i: string; + s: string; + p: string; + kt: string | string[] | string[][]; + k: string[]; + nt: string | string[] | string[][]; + n: string[]; + bt: string; + br: string[]; + ba: string[]; + c: string[]; + a: unknown; + }; + RPY_V_1: { + v: string; + t: string; + d: string; + dt: string; + r: string; + a: unknown; + }; + RPY_V_2: { + v: string; + t: string; + d: string; + i: string; + dt: string; + r: string; + a: unknown; + }; + Credential: { + sad: + | components['schemas']['ACDC_V_1'] + | components['schemas']['ACDC_V_2']; + atc: string; + iss: components['schemas']['IssEvent']; + issatc: string; + pre: string; + schema: components['schemas']['Schema']; + chains: { + [key: string]: unknown; + }[]; + status: components['schemas']['CredentialState']; + anchor: components['schemas']['Anchor']; + anc: + | components['schemas']['IXN_V_1'] + | components['schemas']['IXN_V_2'] + | components['schemas']['ICP_V_1'] + | components['schemas']['ICP_V_2'] + | components['schemas']['ROT_V_1'] + | components['schemas']['ROT_V_2'] + | components['schemas']['DIP_V_1'] + | components['schemas']['DIP_V_2'] + | components['schemas']['DRT_V_1'] + | components['schemas']['DRT_V_2']; + ancatc: string; + }; + OperationStatus: { + code: number; + message: string; + details?: { + [key: string]: unknown; + } | null; + }; + Operation: { + name: string; + error?: components['schemas']['OperationStatus']; + done?: boolean; + metadata?: Record; + response?: Record; + }; + EmptyDict: Record; + CredentialStateIssOrRev: { + vn: unknown; + i: string; + s: string; + d: string; + ri: string; + a: components['schemas']['Seal']; + dt: string; + /** @enum {unknown} */ + et: CredentialStateIssOrRevEt; + ra: components['schemas']['EmptyDict']; + }; + RaFields: { + i: string; + s: string; + d: string; + }; + CredentialStateBisOrBrv: { + vn: unknown; + i: string; + s: string; + d: string; + ri: string; + a: components['schemas']['Seal']; + dt: string; + /** @enum {unknown} */ + et: CredentialStateBisOrBrvEt; + ra: components['schemas']['RaFields']; + }; + CredentialState: + | components['schemas']['CredentialStateIssOrRev'] + | components['schemas']['CredentialStateBisOrBrv']; + Registry: { + name: string; + regk: string; + pre: string; + state: components['schemas']['CredentialState']; + }; + StateEERecord: { + /** @default 0 */ + s: string; + /** @default */ + d: string; + br?: unknown[]; + ba?: unknown[]; + }; + KeyStateRecord: { + vn?: number[]; + /** @default */ + i: string; + /** @default 0 */ + s: string; + /** @default */ + p: string; + /** @default */ + d: string; + /** @default 0 */ + f: string; + /** @default */ + dt: string; + /** @default */ + et: string; + /** @default 0 */ + kt: string; + k: string[]; + /** @default 0 */ + nt: string; + n: string[]; + /** @default 0 */ + bt: string; + b: string[]; + c: string[]; + ee: components['schemas']['StateEERecord']; + /** @default */ + di: string; + }; + Controller: { + state: components['schemas']['KeyStateRecord']; + ee: + | components['schemas']['ICP_V_1'] + | components['schemas']['ICP_V_2'] + | components['schemas']['ROT_V_1'] + | components['schemas']['ROT_V_2'] + | components['schemas']['DIP_V_1'] + | components['schemas']['DIP_V_2'] + | components['schemas']['DRT_V_1'] + | components['schemas']['DRT_V_2']; + }; + AgentResourceResult: { + agent: components['schemas']['KeyStateRecord']; + controller: components['schemas']['Controller']; + pidx: number; + /** @default null */ + ridx: number | null; + /** @default null */ + sxlt: string | null; + }; + SaltyState: { + tier: components['schemas']['Tier']; + /** @default */ + sxlt: string; + /** @default 0 */ + pidx: number; + /** @default 0 */ + kidx: number; + /** @default */ + stem: string; + /** @default */ + dcode: string; + icodes: string[]; + ncodes: string[]; + /** @default false */ + transferable: boolean; + }; + RandyKeyState: { + prxs: string[]; + nxts: string[]; + }; + HabState: { + name: string; + prefix: string; + icp_dt: string; + state: components['schemas']['KeyStateRecord']; + /** @default null */ + transferable: boolean | null; + /** @default null */ + windexes: string[] | null; + }; + GroupKeyState: { + mhab: components['schemas']['Identifier']; + keys: string[]; + ndigs: string[]; + }; + ExternState: { + extern_type: string; + pidx: number; + } & { + [key: string]: unknown; + }; + Identifier: { + name: string; + prefix: string; + icp_dt: string; + state: components['schemas']['KeyStateRecord']; + /** @default null */ + transferable: boolean | null; + /** @default null */ + windexes: string[] | null; + } & ( + | { + salty: components['schemas']['SaltyState']; + } + | { + randy: components['schemas']['RandyKeyState']; + } + | { + group: components['schemas']['GroupKeyState']; + } + | { + extern: components['schemas']['ExternState']; + } + ); + /** + * @description Tier of key material + * @enum {string} + */ + Tier: Tier; + OOBI: { + /** @enum {string} */ + role: OOBIRole; + oobis: string[]; + }; + EndRole: { + cid: string; + role: string; + eid: string; + }; + Rpy: + | components['schemas']['RPY_V_1'] + | components['schemas']['RPY_V_2']; + Challenge: { + words: string[]; + dt?: string; + said?: string; + authenticated?: boolean; + }; + MemberEnds: { + /** @default null */ + agent: { + [key: string]: string; + } | null; + /** @default null */ + controller: { + [key: string]: string; + } | null; + /** @default null */ + witness: { + [key: string]: string; + } | null; + /** @default null */ + registrar: { + [key: string]: string; + } | null; + /** @default null */ + watcher: { + [key: string]: string; + } | null; + /** @default null */ + judge: { + [key: string]: string; + } | null; + /** @default null */ + juror: { + [key: string]: string; + } | null; + /** @default null */ + peer: { + [key: string]: string; + } | null; + /** @default null */ + mailbox: { + [key: string]: string; + } | null; + }; + WellKnown: { + url: string; + dt: string; + }; + Contact: { + id: string; + alias?: string; + oobi?: string; + ends?: components['schemas']['MemberEnds']; + challenges?: components['schemas']['Challenge'][]; + wellKnowns?: components['schemas']['WellKnown'][]; + } & { + [key: string]: unknown; + }; + AidRecord: { + aid: string; + ends: components['schemas']['MemberEnds']; + }; + GroupMember: { + signing: components['schemas']['AidRecord'][]; + rotation: components['schemas']['AidRecord'][]; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export enum IssEventT { + iss = 'iss', + bis = 'bis', +} +export enum CredentialStateIssOrRevEt { + iss = 'iss', + rev = 'rev', +} +export enum CredentialStateBisOrBrvEt { + bis = 'bis', + brv = 'brv', +} +export enum Tier { + low = 'low', + med = 'med', + high = 'high', +} +export enum OOBIRole { + controller = 'controller', + witness = 'witness', + registrar = 'registrar', + watcher = 'watcher', + judge = 'judge', + juror = 'juror', + peer = 'peer', + mailbox = 'mailbox', + agent = 'agent', +} diff --git a/packages/signify-ts/test-integration/challenge.test.ts b/packages/signify-ts/test-integration/challenge.test.ts new file mode 100644 index 00000000..eba6fafe --- /dev/null +++ b/packages/signify-ts/test-integration/challenge.test.ts @@ -0,0 +1,99 @@ +import { assert, test } from 'vitest'; +import { Serder } from 'signify-ts'; +import { + assertOperations, + getOrCreateClients, + resolveOobi, + waitOperation, +} from './utils/test-util.ts'; + +test('challenge', async () => { + const [client1, client2] = await getOrCreateClients(2); + + // Generate challenge words + const challenge1_small = await client1.challenges().generate(128); + assert.equal(challenge1_small.words.length, 12); + const challenge1_big = await client1.challenges().generate(256); + assert.equal(challenge1_big.words.length, 24); + + // Create two identifiers, one for each client + const icpResult1 = await client1.identifiers().create('alice', { + toad: 3, + wits: [ + 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM', + 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX', + ], + }); + const { response: aid1 } = await waitOperation( + client1, + await icpResult1.op() + ); + const rpyResult1 = await client1 + .identifiers() + .addEndRole('alice', 'agent', client1!.agent!.pre); + await waitOperation(client1, await rpyResult1.op()); + console.log("Alice's AID:", aid1.i); + + const icpResult2 = await client2.identifiers().create('bob', { + toad: 3, + wits: [ + 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM', + 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX', + ], + }); + const { response: aid2 } = await waitOperation( + client2, + await icpResult2.op() + ); + const rpyResult2 = await client2 + .identifiers() + .addEndRole('bob', 'agent', client2!.agent!.pre); + await waitOperation(client2, await rpyResult2.op()); + + // Exchenge OOBIs + const oobi1 = await client1.oobis().get('alice', 'agent'); + const oobi2 = await client2.oobis().get('bob', 'agent'); + + await resolveOobi(client1, oobi2.oobis[0], 'bob'); + console.log("Client 1 resolved Bob's OOBI"); + await resolveOobi(client2, oobi1.oobis[0], 'alice'); + console.log("Client 2 resolved Alice's OOBI"); + + // List Client 1 contacts + let contacts1 = await client1.contacts().list(); + let bobContact = contacts1.find((contact) => contact.alias === 'bob'); + assert.equal(bobContact?.alias, 'bob'); + assert(Array.isArray(bobContact?.challenges)); + assert.strictEqual(bobContact.challenges.length, 0); + + // Bob responds to Alice challenge + await client2.challenges().respond('bob', aid1.i, challenge1_small.words); + console.log('Bob responded to Alice challenge with signed words'); + + // Alice verifies Bob's response + const verifyOperation = await waitOperation( + client1, + await client1.challenges().verify(aid2.i, challenge1_small.words) + ); + console.log('Alice verified challenge response'); + + //Alice mark response as accepted + const verifyResponse = verifyOperation.response as { + exn: Record; + }; + const exn = new Serder(verifyResponse.exn); + + await client1.challenges().responded(aid2.i, exn.sad.d); + console.log('Alice marked challenge response as accepted'); + + // Check Bob's challenge in conctats + contacts1 = await client1.contacts().list(); + bobContact = contacts1.find((contact) => contact.alias === 'bob'); + + assert(Array.isArray(bobContact?.challenges)); + assert.strictEqual(bobContact?.challenges[0].authenticated, true); + + await assertOperations(client1, client2); +}, 30000); diff --git a/packages/signify-ts/test-integration/credentials.test.ts b/packages/signify-ts/test-integration/credentials.test.ts new file mode 100644 index 00000000..fc5181f1 --- /dev/null +++ b/packages/signify-ts/test-integration/credentials.test.ts @@ -0,0 +1,665 @@ +import { assert, beforeAll, afterAll, test, expect } from 'vitest'; +import { Ilks, Saider, Serder, SignifyClient } from 'signify-ts'; +import { resolveEnvironment } from './utils/resolve-env.ts'; +import { + assertNotifications, + assertOperations, + createAid, + getOrCreateClients, + getOrCreateContact, + markAndRemoveNotification, + resolveOobi, + waitForNotifications, + waitOperation, +} from './utils/test-util.ts'; +import { retry } from './utils/retry.ts'; +import { randomUUID } from 'node:crypto'; +import { step } from './utils/test-step.ts'; +const { vleiServerUrl } = resolveEnvironment(); + +const QVI_SCHEMA_SAID = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; +const LE_SCHEMA_SAID = 'ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY'; +const vLEIServerHostUrl = `${vleiServerUrl}/oobi`; +const QVI_SCHEMA_URL = `${vLEIServerHostUrl}/${QVI_SCHEMA_SAID}`; +const LE_SCHEMA_URL = `${vLEIServerHostUrl}/${LE_SCHEMA_SAID}`; + +interface Aid { + name: string; + prefix: string; + oobi: string; +} + +function createTimestamp() { + return new Date().toISOString().replace('Z', '000+00:00'); +} + +let issuerClient: SignifyClient; +let holderClient: SignifyClient; +let verifierClient: SignifyClient; +let legalEntityClient: SignifyClient; + +let issuerAid: Aid; +let holderAid: Aid; +let verifierAid: Aid; +let legalEntityAid: Aid; + +let applySaid: string; +let offerSaid: string; +let agreeSaid: string; + +beforeAll(async () => { + [issuerClient, holderClient, verifierClient, legalEntityClient] = + await getOrCreateClients(4); +}); + +beforeAll(async () => { + [issuerAid, holderAid, verifierAid, legalEntityAid] = await Promise.all([ + createAid(issuerClient, 'issuer'), + createAid(holderClient, 'holder'), + createAid(verifierClient, 'verifier'), + createAid(legalEntityClient, 'legal-entity'), + ]); +}); + +beforeAll(async () => { + await Promise.all([ + getOrCreateContact(issuerClient, 'holder', holderAid.oobi), + getOrCreateContact(issuerClient, 'verifier', verifierAid.oobi), + getOrCreateContact(holderClient, 'issuer', issuerAid.oobi), + getOrCreateContact(holderClient, 'verifier', verifierAid.oobi), + getOrCreateContact(holderClient, 'legal-entity', legalEntityAid.oobi), + getOrCreateContact(verifierClient, 'issuer', issuerAid.oobi), + getOrCreateContact(verifierClient, 'holder', holderAid.oobi), + getOrCreateContact(legalEntityClient, 'holder', holderAid.oobi), + ]); +}); + +afterAll(async () => { + await assertOperations( + issuerClient, + holderClient, + verifierClient, + legalEntityClient + ); + await assertNotifications( + issuerClient, + holderClient, + verifierClient, + legalEntityClient + ); +}); + +test('single signature credentials', { timeout: 90000 }, async () => { + await step('Resolve schema oobis', async () => { + await Promise.all([ + resolveOobi(issuerClient, QVI_SCHEMA_URL), + resolveOobi(issuerClient, LE_SCHEMA_URL), + resolveOobi(holderClient, QVI_SCHEMA_URL), + resolveOobi(holderClient, LE_SCHEMA_URL), + resolveOobi(verifierClient, QVI_SCHEMA_URL), + resolveOobi(verifierClient, LE_SCHEMA_URL), + resolveOobi(legalEntityClient, QVI_SCHEMA_URL), + resolveOobi(legalEntityClient, LE_SCHEMA_URL), + ]); + }); + + const registry = await step('Create registry', async () => { + const registryName = 'vLEI-test-registry'; + const updatedRegistryName = 'vLEI-test-registry-1'; + const regResult = await issuerClient + .registries() + .create({ name: issuerAid.name, registryName: registryName }); + + await waitOperation(issuerClient, await regResult.op()); + let registries = await retry(async () => { + const result = await issuerClient.registries().list(issuerAid.name); + assert(result.length >= 1, 'Expected at least one registry'); + return result; + }); + const registry: { name: string; regk: string } = registries[0]; + assert.equal(registries.length, 1); + assert.equal(registry.name, registryName); + + await issuerClient + .registries() + .rename(issuerAid.name, registryName, updatedRegistryName); + + registries = await retry(async () => { + const result = await issuerClient.registries().list(issuerAid.name); + assert(result.length >= 1, 'Expected at least one registry'); + return result; + }); + const updateRegistry: { name: string; regk: string } = registries[0]; + assert.equal(registries.length, 1); + assert.equal(updateRegistry.name, updatedRegistryName); + + return updateRegistry; + }); + + await step('issuer can get schemas', async () => { + const issuerQviSchema = await issuerClient + .schemas() + .get(QVI_SCHEMA_SAID); + + assert.equal(issuerQviSchema.$id, QVI_SCHEMA_SAID); + + const issuerLeSchema = await issuerClient.schemas().get(LE_SCHEMA_SAID); + + assert.equal(issuerLeSchema.$id, LE_SCHEMA_SAID); + }); + + await step('holder can list schemas', async () => { + const holderSchemas = await holderClient.schemas().list(); + assert.equal(holderSchemas.length, 2); + }); + + const qviCredentialId = await step('create QVI credential', async () => { + const vcdata = { + LEI: '5493001KJTIIGC8Y1R17', + }; + + const issResult = await issuerClient + .credentials() + .issue(issuerAid.name, { + ri: registry.regk, + s: QVI_SCHEMA_SAID, + a: { + i: holderAid.prefix, + ...vcdata, + }, + }); + + await waitOperation(issuerClient, issResult.op); + return issResult.acdc.sad.d as string; + }); + + await step('issuer list credentials', async () => { + const issuerCredentials = await issuerClient.credentials().list(); + assertLength(issuerCredentials, 1); + assert(issuerCredentials.length >= 1); + assert.equal(issuerCredentials[0].sad.s, QVI_SCHEMA_SAID); + assert.equal(issuerCredentials[0].sad.i, issuerAid.prefix); + assert.equal(issuerCredentials[0].status.s, '0'); + }); + + function assertLength(obj: unknown, length: number) { + assert(obj); + assert(typeof obj === 'object'); + assert('length' in obj); + assert.strictEqual(obj.length, length); + } + await step('issuer list credentials with filter', async () => { + assertLength( + await issuerClient + .credentials() + .list({ filter: { '-i': issuerAid.prefix } }), + 1 + ); + + assertLength( + await issuerClient + .credentials() + .list({ filter: { '-s': QVI_SCHEMA_SAID } }), + 1 + ); + + assertLength( + await issuerClient + .credentials() + .list({ filter: { '-a-i': holderAid.prefix } }), + 1 + ); + + assertLength( + await issuerClient.credentials().list({ + filter: { + '-i': issuerAid.prefix, + '-s': QVI_SCHEMA_SAID, + '-a-i': holderAid.prefix, + }, + }), + 1 + ); + + assertLength( + await issuerClient.credentials().list({ + filter: { + '-i': randomUUID(), + '-s': QVI_SCHEMA_SAID, + '-a-i': holderAid.prefix, + }, + }), + 0 + ); + }); + + await step('issuer get credential by id', async () => { + const issuerCredential = await issuerClient + .credentials() + .get(qviCredentialId); + assert(issuerCredential !== undefined); + assert.equal(issuerCredential.sad.s, QVI_SCHEMA_SAID); + assert.equal(issuerCredential.sad.i, issuerAid.prefix); + assert.equal(issuerCredential.status.s, '0'); + }); + + await step('issuer IPEX grant', async () => { + const dt = createTimestamp(); + const issuerCredential = await issuerClient + .credentials() + .get(qviCredentialId); + assert(issuerCredential !== undefined); + + const [grant, gsigs, gend] = await issuerClient.ipex().grant({ + senderName: issuerAid.name, + acdc: new Serder(issuerCredential.sad), + anc: new Serder(issuerCredential.anc), + iss: new Serder(issuerCredential.iss), + ancAttachment: issuerCredential.ancatc, + recipient: holderAid.prefix, + datetime: dt, + }); + + const op = await issuerClient + .ipex() + .submitGrant(issuerAid.name, grant, gsigs, gend, [ + holderAid.prefix, + ]); + await waitOperation(issuerClient, op); + }); + + await step( + 'holder can get the credential status before or without holding', + async () => { + const state = await retry(async () => + holderClient.credentials().state(registry.regk, qviCredentialId) + ); + assert.equal(state.i, qviCredentialId); + assert.equal(state.ri, registry.regk); + assert.equal(state.et, Ilks.iss); + } + ); + + await step('holder IPEX admit', async () => { + const holderNotifications = await waitForNotifications( + holderClient, + '/exn/ipex/grant' + ); + const grantNotification = holderNotifications[0]; // should only have one notification right now + + const [admit, sigs, aend] = await holderClient.ipex().admit({ + senderName: holderAid.name, + message: '', + grantSaid: grantNotification.a.d!, + recipient: issuerAid.prefix, + datetime: createTimestamp(), + }); + const op = await holderClient + .ipex() + .submitAdmit(holderAid.name, admit, sigs, aend, [issuerAid.prefix]); + await waitOperation(holderClient, op); + + await markAndRemoveNotification(holderClient, grantNotification); + }); + + await step('issuer IPEX grant response', async () => { + const issuerNotifications = await waitForNotifications( + issuerClient, + '/exn/ipex/admit' + ); + await markAndRemoveNotification(issuerClient, issuerNotifications[0]); + }); + + await step('holder has credential', async () => { + const holderCredential = await retry(async () => { + const result = await holderClient + .credentials() + .get(qviCredentialId); + assert(result !== undefined); + return result; + }); + assert.equal(holderCredential.sad.s, QVI_SCHEMA_SAID); + assert.equal(holderCredential.sad.i, issuerAid.prefix); + assert.equal(holderCredential.status.s, '0'); + assert(holderCredential.atc !== undefined); + }); + + await step('verifier IPEX apply', async () => { + const [apply, sigs, _] = await verifierClient.ipex().apply({ + senderName: verifierAid.name, + schemaSaid: QVI_SCHEMA_SAID, + attributes: { LEI: '5493001KJTIIGC8Y1R17' }, + recipient: holderAid.prefix, + datetime: createTimestamp(), + }); + + const op = await verifierClient + .ipex() + .submitApply(verifierAid.name, apply, sigs, [holderAid.prefix]); + await waitOperation(verifierClient, op); + }); + + await step('holder IPEX apply receive and offer', async () => { + const holderNotifications = await waitForNotifications( + holderClient, + '/exn/ipex/apply' + ); + + const holderApplyNote = holderNotifications[0]; + assert(holderApplyNote.a.d); + + const apply = await holderClient.exchanges().get(holderApplyNote.a.d); + applySaid = apply.exn.d; + + const filter: { [x: string]: any } = { '-s': apply.exn.a.s }; + for (const key in apply.exn.a.a) { + filter[`-a-${key}`] = apply.exn.a.a[key]; + } + + const matchingCreds = await holderClient.credentials().list({ filter }); + assert.strictEqual(matchingCreds.length, 1); + + await markAndRemoveNotification(holderClient, holderNotifications[0]); + + const [offer, sigs, end] = await holderClient.ipex().offer({ + senderName: holderAid.name, + recipient: verifierAid.prefix, + acdc: new Serder(matchingCreds[0].sad), + applySaid: applySaid, + datetime: createTimestamp(), + }); + + const op = await holderClient + .ipex() + .submitOffer(holderAid.name, offer, sigs, end, [ + verifierAid.prefix, + ]); + await waitOperation(holderClient, op); + }); + + await step('verifier receive offer and agree', async () => { + const verifierNotifications = await waitForNotifications( + verifierClient, + '/exn/ipex/offer' + ); + + const verifierOfferNote = verifierNotifications[0]; + assert(verifierOfferNote.a.d); + + const offer = await verifierClient + .exchanges() + .get(verifierOfferNote.a.d); + offerSaid = offer.exn.d; + + assert.strictEqual(offer.exn.p, applySaid); + assert.strictEqual(offer.exn.e.acdc.a.LEI, '5493001KJTIIGC8Y1R17'); + + await markAndRemoveNotification(verifierClient, verifierOfferNote); + + const [agree, sigs, _] = await verifierClient.ipex().agree({ + senderName: verifierAid.name, + recipient: holderAid.prefix, + offerSaid: offerSaid, + datetime: createTimestamp(), + }); + + const op = await verifierClient + .ipex() + .submitAgree(verifierAid.name, agree, sigs, [holderAid.prefix]); + await waitOperation(verifierClient, op); + }); + + await step('holder IPEX receive agree and grant/present', async () => { + const holderNotifications = await waitForNotifications( + holderClient, + '/exn/ipex/agree' + ); + + const holderAgreeNote = holderNotifications[0]; + assert(holderAgreeNote.a.d); + + const agree = await holderClient.exchanges().get(holderAgreeNote.a.d); + agreeSaid = agree.exn.d; + + assert.strictEqual(agree.exn.p, offerSaid); + + await markAndRemoveNotification(holderClient, holderAgreeNote); + + const holderCredential = await holderClient + .credentials() + .get(qviCredentialId); + + const [grant2, gsigs2, gend2] = await holderClient.ipex().grant({ + senderName: holderAid.name, + recipient: verifierAid.prefix, + acdc: new Serder(holderCredential.sad), + anc: new Serder(holderCredential.anc), + iss: new Serder(holderCredential.iss), + acdcAttachment: holderCredential.atc, + ancAttachment: holderCredential.ancatc, + issAttachment: holderCredential.issatc, + agreeSaid: agreeSaid, + datetime: createTimestamp(), + }); + + const op = await holderClient + .ipex() + .submitGrant(holderAid.name, grant2, gsigs2, gend2, [ + verifierAid.prefix, + ]); + await waitOperation(holderClient, op); + }); + + await step('verifier receives IPEX grant', async () => { + const verifierNotifications = await waitForNotifications( + verifierClient, + '/exn/ipex/grant' + ); + + const verifierGrantNote = verifierNotifications[0]; + assert(verifierGrantNote.a.d); + + const grant = await holderClient.exchanges().get(verifierGrantNote.a.d); + assert.strictEqual(grant.exn.p, agreeSaid); + + const [admit3, sigs3, aend3] = await verifierClient.ipex().admit({ + senderName: verifierAid.name, + message: '', + grantSaid: verifierGrantNote.a.d!, + recipient: holderAid.prefix, + datetime: createTimestamp(), + }); + + const op = await verifierClient + .ipex() + .submitAdmit(verifierAid.name, admit3, sigs3, aend3, [ + holderAid.prefix, + ]); + await waitOperation(verifierClient, op); + + await markAndRemoveNotification(verifierClient, verifierGrantNote); + + const verifierCredential = await retry(async () => + verifierClient.credentials().get(qviCredentialId) + ); + + assert.equal(verifierCredential.sad.s, QVI_SCHEMA_SAID); + assert.equal(verifierCredential.sad.i, issuerAid.prefix); + assert.equal(verifierCredential.status.s, '0'); // 0 = issued + }); + + await step('holder IPEX present response', async () => { + const holderNotifications = await waitForNotifications( + holderClient, + '/exn/ipex/admit' + ); + + await markAndRemoveNotification(holderClient, holderNotifications[0]); + }); + + const holderRegistry: { regk: string } = await step( + 'holder create registry for LE credential', + async () => { + const registryName = 'vLEI-test-registry'; + const regResult = await holderClient + .registries() + .create({ name: holderAid.name, registryName: registryName }); + + await waitOperation(holderClient, await regResult.op()); + return await retry(async () => { + const registries = await holderClient + .registries() + .list(holderAid.name); + + assert(registries.length >= 1); + return registries[0]; + }); + } + ); + + const leCredentialId = await step( + 'holder create LE (chained) credential', + async () => { + const qviCredential = await holderClient + .credentials() + .get(qviCredentialId); + + const result = await holderClient + .credentials() + .issue(holderAid.name, { + a: { + i: legalEntityAid.prefix, + LEI: '5493001KJTIIGC8Y1R17', + }, + ri: holderRegistry.regk, + s: LE_SCHEMA_SAID, + r: Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, + })[1], + e: Saider.saidify({ + d: '', + qvi: { + n: qviCredential.sad.d, + s: qviCredential.sad.s, + }, + })[1], + }); + + await waitOperation(holderClient, result.op); + return result.acdc.sad.d; + } + ); + + await step('LE credential IPEX grant', async () => { + const dt = createTimestamp(); + const leCredential = await holderClient + .credentials() + .get(leCredentialId); + assert(leCredential !== undefined); + + const [grant, gsigs, gend] = await holderClient.ipex().grant({ + senderName: holderAid.name, + acdc: new Serder(leCredential.sad), + anc: new Serder(leCredential.anc), + iss: new Serder(leCredential.iss), + ancAttachment: leCredential.ancatc, + recipient: legalEntityAid.prefix, + datetime: dt, + }); + + const op = await holderClient + .ipex() + .submitGrant(holderAid.name, grant, gsigs, gend, [ + legalEntityAid.prefix, + ]); + await waitOperation(holderClient, op); + }); + + await step('Legal Entity IPEX admit', async () => { + const notifications = await waitForNotifications( + legalEntityClient, + '/exn/ipex/grant' + ); + const grantNotification = notifications[0]; + + const [admit, sigs, aend] = await legalEntityClient.ipex().admit({ + senderName: legalEntityAid.name, + message: '', + grantSaid: grantNotification.a.d!, + recipient: holderAid.prefix, + datetime: createTimestamp(), + }); + + const op = await legalEntityClient + .ipex() + .submitAdmit(legalEntityAid.name, admit, sigs, aend, [ + holderAid.prefix, + ]); + await waitOperation(legalEntityClient, op); + + await markAndRemoveNotification(legalEntityClient, grantNotification); + }); + + await step('LE credential IPEX grant response', async () => { + const notifications = await waitForNotifications( + holderClient, + '/exn/ipex/admit' + ); + await markAndRemoveNotification(holderClient, notifications[0]); + }); + + await step('Legal Entity has chained credential', async () => { + const legalEntityCredential = await retry(async () => + legalEntityClient.credentials().get(leCredentialId) + ); + assert.equal(legalEntityCredential.sad.s, LE_SCHEMA_SAID); + assert.equal(legalEntityCredential.sad.i, holderAid.prefix); + + if ( + 'a' in legalEntityCredential.sad && + legalEntityCredential.sad.a !== undefined + ) { + assert.equal(legalEntityCredential.sad.a !== undefined, true); + assert.equal(legalEntityCredential.sad.a.i, legalEntityAid.prefix); + } + + assert.equal(legalEntityCredential.status.s, '0'); + assert(Array.isArray(legalEntityCredential.chains)); + assert(legalEntityCredential.chains.length > 0); + const firstChain = legalEntityCredential.chains[0] as { + sad: { d: string }; + }; + assert.equal(firstChain.sad.d, qviCredentialId); + assert(legalEntityCredential.atc !== undefined); + }); + + await step('Issuer revoke QVI credential', async () => { + const revokeOperation = await issuerClient + .credentials() + .revoke(issuerAid.name, qviCredentialId); + + await waitOperation(issuerClient, revokeOperation.op); + const issuerCredential = await issuerClient + .credentials() + .get(qviCredentialId); + + assert.equal(issuerCredential.status.s, '1'); + }); + + await step('Holder deletes LE credential', async () => { + await holderClient.credentials().delete(leCredentialId); + await expect(async () => { + await holderClient.credentials().get(leCredentialId); + }).rejects.toThrowError( + `HTTP GET /credentials/${leCredentialId} - 404 Not Found - {"title": "404 Not Found", "description": "credential for said ${leCredentialId} not found."}` + ); + + assert.equal((await holderClient.credentials().list()).length, 1); + }); +}); diff --git a/packages/signify-ts/test-integration/delegation-multisig.test.ts b/packages/signify-ts/test-integration/delegation-multisig.test.ts new file mode 100644 index 00000000..553466cd --- /dev/null +++ b/packages/signify-ts/test-integration/delegation-multisig.test.ts @@ -0,0 +1,339 @@ +import { assert, test } from 'vitest'; +import signify from 'signify-ts'; +import { + assertNotifications, + assertOperations, + createAID, + createTimestamp, + getOrCreateClient, + getOrCreateContact, + markAndRemoveNotification, + resolveOobi, + waitAndMarkNotification, + waitForNotifications, + waitOperation, +} from './utils/test-util.ts'; +import { + acceptMultisigIncept, + addEndRoleMultisig, + delegateMultisig, + startMultisigIncept, +} from './utils/multisig-utils.ts'; +import { step } from './utils/test-step.ts'; + +const delegatorGroupName = 'delegator_group'; +const delegateeGroupName = 'delegatee_group'; +const delegator1Name = 'delegator1'; +const delegator2Name = 'delegator2'; +const delegatee1Name = 'delegatee1'; +const delegatee2Name = 'delegatee2'; + +test('delegation-multisig', async () => { + await signify.ready(); + // Boot three clients + const [ + delegator1Client, + delegator2Client, + delegatee1Client, + delegatee2Client, + ] = await step('Creating single sig clients', async () => { + return await Promise.all([ + getOrCreateClient(), + getOrCreateClient(), + getOrCreateClient(), + getOrCreateClient(), + ]); + }); + + // Create delegator and delegatee identifiers clients + const [delegator1Aid, delegator2Aid, delegatee1Aid, delegatee2Aid] = + await step('Creating single sig aids', async () => { + return await Promise.all([ + createAID(delegator1Client, delegator1Name), + createAID(delegator2Client, delegator2Name), + createAID(delegatee1Client, delegatee1Name), + createAID(delegatee2Client, delegatee2Name), + ]); + }); + + // Exchange OOBIs + const [delegator1Oobi, delegator2Oobi, delegatee1Oobi, delegatee2Oobi] = + await step('Getting OOBIs before resolving...', async () => { + return await Promise.all([ + await delegator1Client.oobis().get(delegator1Name, 'agent'), + await delegator2Client.oobis().get(delegator2Name, 'agent'), + await delegatee1Client.oobis().get(delegatee1Name, 'agent'), + await delegatee2Client.oobis().get(delegatee2Name, 'agent'), + ]); + }); + + await step('Resolving OOBIs', async () => { + await Promise.all([ + resolveOobi( + delegator1Client, + delegator2Oobi.oobis[0], + delegator2Name + ), + resolveOobi( + delegator2Client, + delegator1Oobi.oobis[0], + delegator1Name + ), + resolveOobi( + delegatee1Client, + delegatee2Oobi.oobis[0], + delegatee2Name + ), + resolveOobi( + delegatee2Client, + delegatee1Oobi.oobis[0], + delegatee1Name + ), + ]); + }); + console.log( + `${delegator1Name}(${delegator1Aid.prefix}) and ${delegatee1Name}(${delegatee1Aid.prefix}) resolved ${delegator2Name}(${delegator2Aid.prefix}) and ${delegatee2Name}(${delegatee2Aid.prefix}) OOBIs and vice versa` + ); + + // First member start the creation of a multisig identifier + // Create a multisig AID for the GEDA. + // Skip if a GEDA AID has already been incepted. + const otor1 = await step( + `${delegator1Name}(${delegator1Aid.prefix}) initiated delegator multisig, waiting for ${delegator2Name}(${delegator2Aid.prefix}) to join...`, + async () => { + return await startMultisigIncept(delegator1Client, { + groupName: delegatorGroupName, + localMemberName: delegator1Aid.name, + participants: [delegator1Aid.prefix, delegator2Aid.prefix], + isith: 2, + nsith: 2, + toad: 2, + wits: [ + 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM', + 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX', + ], + }); + } + ); + + const [ntor] = await waitForNotifications( + delegator2Client, + '/multisig/icp', + { + maxSleep: 10000, + minSleep: 1000, + maxRetries: undefined, + timeout: 30000, + } + ); + await markAndRemoveNotification(delegator2Client, ntor); + assert(ntor.a.d); + const otor2 = await acceptMultisigIncept(delegator2Client, { + localMemberName: delegator2Aid.name, + groupName: delegatorGroupName, + msgSaid: ntor.a.d, + }); + + const torpre = otor1.name.split('.')[1]; + await Promise.all([ + waitOperation(delegator1Client, otor1), + waitOperation(delegator2Client, otor2), + ]); + + const adelegatorGroupName1 = await delegator1Client + .identifiers() + .get(delegatorGroupName); + const adelegatorGroupName2 = await delegator2Client + .identifiers() + .get(delegatorGroupName); + + assert.equal(adelegatorGroupName1.prefix, adelegatorGroupName2.prefix); + assert.equal(adelegatorGroupName1.name, adelegatorGroupName2.name); + const adelegatorGroupName = adelegatorGroupName1; + + //Resolve delegator OOBI + const delegatorGroupNameOobi = await step( + `Add and resolve delegator OOBI ${delegatorGroupName}(${adelegatorGroupName.prefix})`, + async () => { + const timestamp = createTimestamp(); + const opList1 = await addEndRoleMultisig( + delegator1Client, + delegatorGroupName, + delegator1Aid, + [delegator2Aid], + adelegatorGroupName, + timestamp, + true + ); + const opList2 = await addEndRoleMultisig( + delegator2Client, + delegatorGroupName, + delegator2Aid, + [delegator1Aid], + adelegatorGroupName, + timestamp + ); + + await Promise.all( + opList1.map((op) => waitOperation(delegator1Client, op)) + ); + await Promise.all( + opList2.map((op) => waitOperation(delegator2Client, op)) + ); + + await waitAndMarkNotification(delegator1Client, '/multisig/rpy'); + await waitAndMarkNotification(delegator2Client, '/multisig/rpy'); + + const [odelegatorGroupName1, odelegatorGroupName2] = + await Promise.all([ + delegator1Client + .oobis() + .get(adelegatorGroupName.name, 'agent'), + delegator2Client + .oobis() + .get(adelegatorGroupName.name, 'agent'), + ]); + + assert.equal(odelegatorGroupName1.role, odelegatorGroupName2.role); + assert.equal( + odelegatorGroupName1.oobis[0], + odelegatorGroupName2.oobis[0] + ); + + return odelegatorGroupName1.oobis[0]; + } + ); + + const oobiGtor = delegatorGroupNameOobi.split('/agent/')[0]; + await Promise.all([ + getOrCreateContact( + delegatee1Client, + adelegatorGroupName.name, + oobiGtor + ), + getOrCreateContact( + delegatee2Client, + adelegatorGroupName.name, + oobiGtor + ), + ]); + + const opDelegatee1 = await step( + `${delegatee1Name}(${delegatee1Aid.prefix}) initiated delegatee multisig, waiting for ${delegatee2Name}(${delegatee2Aid.prefix}) to join...`, + async () => { + return await startMultisigIncept(delegatee1Client, { + groupName: delegateeGroupName, + localMemberName: delegatee1Aid.name, + participants: [delegatee1Aid.prefix, delegatee2Aid.prefix], + isith: 2, + nsith: 2, + toad: 2, + delpre: torpre, + wits: [ + 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM', + 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX', + ], + }); + } + ); + + // Second member of delegatee check notifications and join the multisig + const [ntee] = await waitForNotifications( + delegatee2Client, + '/multisig/icp' + ); + await markAndRemoveNotification(delegatee2Client, ntee); + assert(ntee.a.d); + + const opDelegatee2 = await acceptMultisigIncept(delegatee2Client, { + localMemberName: delegatee2Aid.name, + groupName: delegateeGroupName, + msgSaid: ntee.a.d, + }); + + console.log(`${delegatee2Name} joined multisig, waiting for delegator...`); + + const agtee1 = await delegatee1Client.identifiers().get(delegateeGroupName); + const agtee2 = await delegatee2Client.identifiers().get(delegateeGroupName); + + assert.equal(agtee1.prefix, agtee2.prefix); + assert.equal(agtee1.name, agtee2.name); + + const teepre = opDelegatee1.name.split('.')[1]; + assert.equal(opDelegatee2.name.split('.')[1], teepre); + console.log('Delegatee prefix:', teepre); + + await step('delegator anchors/approves delegation', async () => { + // GEDA anchors delegation with an interaction event. + const anchor = { + i: teepre, + s: '0', + d: teepre, + }; + const delApprOp1 = await delegateMultisig( + delegator1Client, + delegator1Aid, + [delegator2Aid], + adelegatorGroupName, + anchor, + true + ); + const delApprOp2 = await delegateMultisig( + delegator2Client, + delegator2Aid, + [delegator1Aid], + adelegatorGroupName, + anchor + ); + const [dresult1, dresult2] = await Promise.all([ + waitOperation(delegator1Client, delApprOp1), + waitOperation(delegator2Client, delApprOp2), + ]); + + assert.equal(dresult1.response, dresult2.response); + + await waitAndMarkNotification(delegator1Client, '/multisig/ixn'); + }); + + const queryOp1 = await delegator1Client + .keyStates() + .query(adelegatorGroupName.prefix, '1'); + const queryOp2 = await delegator2Client + .keyStates() + .query(adelegatorGroupName.prefix, '1'); + + const kstor1 = await waitOperation(delegator1Client, queryOp1); + const kstor2 = await waitOperation(delegator2Client, queryOp2); + + // QARs query the GEDA's key state + const ksteetor1 = await delegatee1Client + .keyStates() + .query(adelegatorGroupName.prefix, '1'); + const ksteetor2 = await delegatee2Client + .keyStates() + .query(adelegatorGroupName.prefix, '1'); + const teeTor1 = await waitOperation(delegatee1Client, ksteetor1); + const teeTor2 = await waitOperation(delegatee2Client, ksteetor2); + + const teeDone1 = await waitOperation(delegatee1Client, opDelegatee1); + const teeDone2 = await waitOperation(delegatee2Client, opDelegatee2); + console.log('Delegated multisig created!'); + + const agtee = await delegatee1Client.identifiers().get(delegateeGroupName); + assert.equal(agtee.prefix, teepre); + + await assertOperations( + delegator1Client, + delegator2Client, + delegatee1Client, + delegatee2Client + ); + await assertNotifications( + delegator1Client, + delegator2Client, + delegatee1Client, + delegatee2Client + ); +}, 600000); diff --git a/packages/signify-ts/test-integration/delegation.test.ts b/packages/signify-ts/test-integration/delegation.test.ts new file mode 100644 index 00000000..0e534c93 --- /dev/null +++ b/packages/signify-ts/test-integration/delegation.test.ts @@ -0,0 +1,92 @@ +import { assert, test } from 'vitest'; +import { + assertOperations, + getOrCreateClients, + getOrCreateContact, + resolveOobi, + waitOperation, +} from './utils/test-util.ts'; +import { retry } from './utils/retry.ts'; +import { step } from './utils/test-step.ts'; + +test('delegation', async () => { + const [client1, client2] = await getOrCreateClients(2); + + // Client 1 create delegator AID + const icpResult1 = await client1.identifiers().create('delegator', { + toad: 3, + wits: [ + 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM', + 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX', + ], + }); + await waitOperation(client1, await icpResult1.op()); + const ator = await client1.identifiers().get('delegator'); + const rpyResult1 = await client1 + .identifiers() + .addEndRole('delegator', 'agent', client1!.agent!.pre); + await waitOperation(client1, await rpyResult1.op()); + console.log("Delegator's AID:", ator.prefix); + + // Client 2 resolves delegator OOBI + console.log('Client 2 resolving delegator OOBI'); + const oobi1 = await client1.oobis().get('delegator', 'agent'); + await resolveOobi(client2, oobi1.oobis[0], 'delegator'); + console.log('OOBI resolved'); + + // Client 2 creates delegate AID + const icpResult2 = await client2 + .identifiers() + .create('delegate', { delpre: ator.prefix }); + const op2 = await icpResult2.op(); + const delegatePrefix = op2.name.split('.')[1]; + console.log("Delegate's prefix:", delegatePrefix); + console.log('Delegate waiting for approval...'); + + // Client 1 approves delegation + const anchor = { + i: delegatePrefix, + s: '0', + d: delegatePrefix, + }; + + await step('delegator approves delegation', async () => { + const result = await retry(async () => { + const apprDelRes = await client1 + .delegations() + .approve('delegator', anchor); + await waitOperation(client1, await apprDelRes.op()); + console.log('Delegator approve delegation submitted'); + return apprDelRes; + }); + assert.equal( + JSON.stringify(result.serder.sad.a[0]), + JSON.stringify(anchor) + ); + }); + + const op3 = await client2.keyStates().query(ator.prefix, '1'); + await waitOperation(client2, op3); + + // Client 2 check approval + await waitOperation(client2, op2); + const aid2 = await client2.identifiers().get('delegate'); + assert.equal(aid2.prefix, delegatePrefix); + console.log('Delegation approved for aid:', aid2.prefix); + + await assertOperations(client1, client2); + const rpyResult2 = await client2 + .identifiers() + .addEndRole('delegate', 'agent', client2!.agent!.pre); + await waitOperation(client2, await rpyResult2.op()); + const oobis = await client2.oobis().get('delegate'); + + const contactId = await getOrCreateContact( + client1, + 'delegate', + oobis.oobis[0].split('/agent/')[0] + ); + + assert.equal(contactId, aid2.prefix); +}, 600000); diff --git a/packages/signify-ts/test-integration/externalModule.test.ts b/packages/signify-ts/test-integration/externalModule.test.ts new file mode 100644 index 00000000..2f53cca5 --- /dev/null +++ b/packages/signify-ts/test-integration/externalModule.test.ts @@ -0,0 +1,28 @@ +import { assert, test } from 'vitest'; +import signify from 'signify-ts'; +import { BIP39Shim } from './modules/bip39_shim.ts'; +import { + assertOperations, + getOrCreateClient, + waitOperation, +} from './utils/test-util.ts'; + +test('bip39_shim', async () => { + const externalModule: signify.ExternalModule = { + type: 'bip39_shim', + name: 'bip39_shim', + module: BIP39Shim, + }; + const client1 = await getOrCreateClient(undefined, [externalModule]); + + const words = new BIP39Shim(0, {}).generateMnemonic(256); + const icpResult = await client1.identifiers().create('aid1', { + algo: signify.Algos.extern, + extern_type: 'bip39_shim', + extern: { mnemonics: words }, + }); + const op = await waitOperation(client1, await icpResult.op()); + assert.equal(op['done'], true); + + await assertOperations(client1); +}, 30000); diff --git a/packages/signify-ts/test-integration/modules/bip39_shim.ts b/packages/signify-ts/test-integration/modules/bip39_shim.ts new file mode 100644 index 00000000..1dd2eb8f --- /dev/null +++ b/packages/signify-ts/test-integration/modules/bip39_shim.ts @@ -0,0 +1,162 @@ +import { mnemonicToSeedSync, generateMnemonic } from 'bip39'; +import { + Diger, + Signer, + MtrDex, + IdentifierManager, + IdentifierManagerResult, + Algos, +} from 'signify-ts'; + +export class BIP39Shim implements IdentifierManager { + private icount: number; + private ncount: number; + private dcode: string | undefined; + private pidx: number = 0; + private kidx: number = 0; + private transferable: boolean; + private stem: string; + private mnemonics: string = ''; + algo: Algos = Algos.extern; + signers: Signer[] = []; + + constructor(pidx: number, kargs: any) { + this.icount = kargs.icount ?? 1; + this.ncount = kargs.ncount ?? 1; + this.pidx = pidx; + this.kidx = kargs.kidx ?? 0; + this.transferable = kargs.transferable ?? true; + this.stem = kargs.stem ?? 'bip39_shim'; + if (kargs.extern != undefined && kargs.extern.mnemonics != undefined) { + this.mnemonics = kargs.extern.mnemonics; + } + this.dcode = kargs.dcode; + } + + params() { + return { + pidx: this.pidx, + kidx: this.kidx, + mnemonics: this.mnemonics, + }; + } + + keys(count: number, kidx: number, transferable: boolean) { + const keys = []; + for (let idx = 0; idx < count; idx++) { + const keyId = `${this.stem}-${this.pidx}-${kidx + idx}`; + const seed = mnemonicToSeedSync(this.mnemonics, keyId); + const signer = new Signer({ + raw: new Uint8Array(seed), + code: MtrDex.Ed25519_Seed, + transferable: transferable, + }); + keys.push(signer); + } + return keys; + } + + async incept(transferable: boolean): Promise { + const signers = this.keys(this.icount, this.kidx, transferable); + const verfers = signers.map((signer) => signer.verfer.qb64); + + const nsigners = this.keys( + this.ncount, + this.kidx + this.icount, + transferable + ); + const digers = nsigners.map( + (nsigner) => + new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64 + ); + return [verfers, digers]; + } + + async rotate( + // TODO: This signature is incompatible with Keeper + // eslint-disable-next-line @typescript-eslint/no-explicit-any + count: any, //number, + transferable: boolean + ): Promise { + const signers = this.keys( + this.ncount, + this.kidx + this.icount, + transferable + ); + const verfers = signers.map((signer) => signer.verfer.qb64); + + this.kidx = this.kidx + this.icount; + this.icount = this.ncount; + + // TODO: Due to incompatible signature. + this.ncount = count as number; + + const nsigners = this.keys( + this.ncount, + this.kidx + this.icount, + this.transferable + ); + const digers = nsigners.map( + (nsigner) => + new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64 + ); + + return [verfers, digers]; + } + + async sign( + ser: Uint8Array, + indexed = true, + indices: number[] | undefined = undefined, + ondices: number[] | undefined = undefined + ) { + const signers = this.keys(this.icount, this.kidx, this.transferable); + + if (indexed) { + const sigers = []; + let i = 0; + for (const [j, signer] of signers.entries()) { + if (indices != undefined) { + i = indices![j]; + if (typeof i != 'number' || i < 0) { + throw new Error( + `Invalid signing index = ${i}, not whole number.` + ); + } + } else { + i = j; + } + let o = 0; + if (ondices != undefined) { + o = ondices![j]; + if ( + (o == undefined || + (typeof o == 'number' && + typeof o != 'number' && + o >= 0))! + ) { + throw new Error( + `Invalid ondex = ${o}, not whole number.` + ); + } + } else { + o = i; + } + sigers.push( + signer.sign(ser, i, o == undefined ? true : false, o) + ); + } + return sigers.map((siger) => siger.qb64); + } else { + const cigars = []; + for (const [, signer] of signers.entries()) { + cigars.push(signer.sign(ser)); + } + return cigars.map((cigar) => cigar.qb64); + } + } + + generateMnemonic(strength: number) { + return generateMnemonic(strength); + } +} diff --git a/packages/signify-ts/test-integration/multisig-holder.test.ts b/packages/signify-ts/test-integration/multisig-holder.test.ts new file mode 100644 index 00000000..27cda57f --- /dev/null +++ b/packages/signify-ts/test-integration/multisig-holder.test.ts @@ -0,0 +1,565 @@ +import { assert, test } from 'vitest'; +import signify, { SignifyClient, Operation, CredentialData } from 'signify-ts'; +import { resolveEnvironment } from './utils/resolve-env.ts'; +import { + assertOperations, + getOrCreateClient, + getOrCreateIdentifier, + waitAndMarkNotification, + waitOperation, + warnNotifications, +} from './utils/test-util.ts'; +import { + acceptMultisigIncept, + startMultisigIncept, +} from './utils/multisig-utils.ts'; +import { retry } from './utils/retry.ts'; + +const { vleiServerUrl } = resolveEnvironment(); +const WITNESS_AIDS = [ + 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM', + 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX', +]; + +const SCHEMA_SAID = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; +const SCHEMA_OOBI = `${vleiServerUrl}/oobi/${SCHEMA_SAID}`; + +const TIME = createTimestamp(); + +test('multisig', async function run() { + await signify.ready(); + // Boot Four clients + const [client1, client2, client3] = await Promise.all([ + getOrCreateClient(), + getOrCreateClient(), + getOrCreateClient(), + ]); + + // Create three identifiers, one for each client + let [aid1, aid2, _aid3] = await Promise.all([ + createAID(client1, 'member1', WITNESS_AIDS), + createAID(client2, 'member2', WITNESS_AIDS), + createAID(client3, 'issuer', WITNESS_AIDS), + ]); + + await createRegistry(client3, 'issuer', 'issuer-reg'); + + // Exchange OOBIs + console.log('Resolving OOBIs'); + const [oobi1, oobi2, oobi3] = await Promise.all([ + client1.oobis().get('member1', 'agent'), + client2.oobis().get('member2', 'agent'), + client3.oobis().get('issuer', 'agent'), + ]); + + let op1 = await client1.oobis().resolve(oobi2.oobis[0], 'member2'); + op1 = await waitOperation(client1, op1); + op1 = await client1.oobis().resolve(oobi3.oobis[0], 'issuer'); + op1 = await waitOperation(client1, op1); + op1 = await client1.oobis().resolve(SCHEMA_OOBI, 'schema'); + op1 = await waitOperation(client1, op1); + console.log('Member1 resolved 3 OOBIs'); + + let op2 = await client2.oobis().resolve(oobi1.oobis[0], 'member1'); + op2 = await waitOperation(client2, op2); + op2 = await client2.oobis().resolve(oobi3.oobis[0], 'issuer'); + op2 = await waitOperation(client2, op2); + op2 = await client2.oobis().resolve(SCHEMA_OOBI, 'schema'); + op2 = await waitOperation(client2, op2); + console.log('Member2 resolved 3 OOBIs'); + + let op3 = await client3.oobis().resolve(oobi1.oobis[0], 'member1'); + op3 = await waitOperation(client3, op3); + op3 = await client3.oobis().resolve(oobi2.oobis[0], 'member2'); + op3 = await waitOperation(client3, op3); + op3 = await client3.oobis().resolve(SCHEMA_OOBI, 'schema'); + op3 = await waitOperation(client3, op3); + console.log('Issuer resolved 3 OOBIs'); + + //// First member start the creation of a multisig identifier + op1 = await startMultisigIncept(client1, { + groupName: 'holder', + localMemberName: aid1.name, + isith: 2, + nsith: 2, + toad: aid1.state.b.length, + wits: aid1.state.b, + participants: [aid1.prefix, aid2.prefix], + }); + console.log('Member1 initiated multisig, waiting for others to join...'); + + // Second member check notifications and join the multisig + let msgSaid = await waitAndMarkNotification(client2, '/multisig/icp'); + console.log('Member2 received exchange message to join multisig'); + op2 = await acceptMultisigIncept(client2, { + groupName: 'holder', + localMemberName: aid2.name, + msgSaid, + }); + console.log('Member2 joined multisig, waiting for others...'); + + // Check for completion + op1 = await waitOperation(client1, op1); + op2 = await waitOperation(client2, op2); + console.log('Multisig created!'); + + const identifiers1 = await client1.identifiers().list(); + assert.equal(identifiers1.aids.length, 2); + + const identifiers2 = await client2.identifiers().list(); + assert.equal(identifiers2.aids.length, 2); + + console.log( + 'Member 1 managed AIDs:\n', + identifiers1.aids[0].name, + `[${identifiers1.aids[0].prefix}]\n`, + identifiers1.aids[1].name, + `[${identifiers1.aids[1].prefix}]` + ); + console.log( + 'Member 2 managed AIDs:\n', + identifiers2.aids[0].name, + `[${identifiers2.aids[0].prefix}]\n`, + identifiers2.aids[1].name, + `[${identifiers2.aids[1].prefix}]` + ); + + // Multisig end role + + aid1 = await client1.identifiers().get('member1'); + aid2 = await client2.identifiers().get('member2'); + const members = await client1.identifiers().members('holder'); + let ghab1 = await client1.identifiers().get('holder'); + const signing = members['signing']; + + const agentEnds1 = signing[0].ends.agent; + if (!agentEnds1) { + throw new Error('signing[0].ends.agent is null or undefined'); + } + const eid1 = Object.keys(agentEnds1)[0]; + + const agentEnds2 = signing[1].ends.agent; + if (!agentEnds2) { + throw new Error('signing[1].ends.agent is null or undefined'); + } + const eid2 = Object.keys(agentEnds2)[0]; + + console.log(`Starting multisig end role authorization for agent ${eid1}`); + + const stamp = createTimestamp(); + + let endRoleRes = await client1 + .identifiers() + .addEndRole('holder', 'agent', eid1, stamp); + op1 = await endRoleRes.op(); + let rpy = endRoleRes.serder; + let sigs = endRoleRes.sigs; + let ghabState1 = ghab1['state']; + let seal = [ + 'SealEvent', + { + i: ghab1['prefix'], + s: ghabState1['ee']['s'], + d: ghabState1['ee']['d'], + }, + ]; + let sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + let roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + let atc = roleims.substring(rpy.size); + let roleembeds = { + rpy: [rpy, atc], + }; + let recp = [aid2['state']].map((state) => state['i']); + let res = await client1 + .exchanges() + .send( + 'member1', + 'multisig', + aid1, + '/multisig/rpy', + { gid: ghab1['prefix'] }, + roleembeds, + recp + ); + console.log( + `Member1 authorized agent role to ${eid1}, waiting for others to authorize...` + ); + + //Member2 check for notifications and join the authorization + msgSaid = await waitAndMarkNotification(client2, '/multisig/rpy'); + console.log( + 'Member2 received exchange message to join the end role authorization' + ); + res = await client2.groups().getRequest(msgSaid); + let exn = res[0].exn; + // stamp, eid and role are provided in the exn message + let rpystamp = exn.e.rpy.dt; + let rpyrole = exn.e.rpy.a.role; + let rpyeid = exn.e.rpy.a.eid; + + endRoleRes = await client2 + .identifiers() + .addEndRole('holder', rpyrole, rpyeid, rpystamp); + op2 = await endRoleRes.op(); + rpy = endRoleRes.serder; + sigs = endRoleRes.sigs; + + let ghab2 = await client2.identifiers().get('holder'); + let ghabState2 = ghab2['state']; + seal = [ + 'SealEvent', + { + i: ghab2['prefix'], + s: ghabState2['ee']['s'], + d: ghabState2['ee']['d'], + }, + ]; + sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + atc = roleims.substring(rpy.size); + roleembeds = { + rpy: [rpy, atc], + }; + recp = [aid1['state']].map((state) => state['i']); + res = await client2 + .exchanges() + .send( + 'member2', + 'multisig', + aid2, + '/multisig/rpy', + { gid: ghab2['prefix'] }, + roleembeds, + recp + ); + console.log( + `Member2 authorized agent role to ${eid1}, waiting for others to authorize...` + ); + // Check for completion + await waitOperation(client1, op1); + await waitOperation(client2, op2); + console.log(`End role authorization for agent ${eid1} completed!`); + + console.log(`Starting multisig end role authorization for agent ${eid2}`); + + endRoleRes = await client1 + .identifiers() + .addEndRole('holder', 'agent', eid2, stamp); + op1 = await endRoleRes.op(); + rpy = endRoleRes.serder; + sigs = endRoleRes.sigs; + + ghab1 = await client1.identifiers().get('holder'); + ghabState1 = ghab1['state']; + seal = [ + 'SealEvent', + { + i: ghab1['prefix'], + s: ghabState1['ee']['s'], + d: ghabState1['ee']['d'], + }, + ]; + sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + atc = roleims.substring(rpy.size); + roleembeds = { + rpy: [rpy, atc], + }; + recp = [aid2['state']].map((state) => state['i']); + res = await client1 + .exchanges() + .send( + 'member1', + 'multisig', + aid1, + '/multisig/rpy', + { gid: ghab1['prefix'] }, + roleembeds, + recp + ); + console.log( + `Member1 authorized agent role to ${eid2}, waiting for others to authorize...` + ); + + //Member2 check for notifications and join the authorization + msgSaid = await waitAndMarkNotification(client2, '/multisig/rpy'); + console.log( + 'Member2 received exchange message to join the end role authorization' + ); + res = await client2.groups().getRequest(msgSaid); + exn = res[0].exn; + // stamp, eid and role are provided in the exn message + rpystamp = exn.e.rpy.dt; + rpyrole = exn.e.rpy.a.role; + rpyeid = exn.e.rpy.a.eid; + endRoleRes = await client2 + .identifiers() + .addEndRole('holder', rpyrole, rpyeid, rpystamp); + op2 = await endRoleRes.op(); + + rpy = endRoleRes.serder; + sigs = endRoleRes.sigs; + + ghab2 = await client2.identifiers().get('holder'); + ghabState2 = ghab2['state']; + seal = [ + 'SealEvent', + { + i: ghab2['prefix'], + s: ghabState2['ee']['s'], + d: ghabState2['ee']['d'], + }, + ]; + + sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + atc = roleims.substring(rpy.size); + roleembeds = { + rpy: [rpy, atc], + }; + recp = [aid1['state']].map((state) => state['i']); + res = await client2 + .exchanges() + .send( + 'member2', + 'multisig', + aid2, + '/multisig/rpy', + { gid: ghab2['prefix'] }, + roleembeds, + recp + ); + + console.log( + `Member2 authorized agent role to ${eid2}, waiting for others to authorize...` + ); + // Check for completion + await waitOperation(client1, op1); + await waitOperation(client2, op2); + console.log(`End role authorization for agent ${eid2} completed!`); + + // Holder resolve multisig OOBI + const oobisRes = await client1.oobis().get('holder', 'agent'); + const oobiMultisig = oobisRes.oobis[0].split('/agent/')[0]; + + op3 = await client3.oobis().resolve(oobiMultisig, 'holder'); + await waitOperation(client3, op3); + console.log(`Issuer resolved multisig holder OOBI`); + + const holderAid = await client1.identifiers().get('holder'); + aid1 = await client1.identifiers().get('member1'); + aid2 = await client2.identifiers().get('member2'); + + console.log(`Issuer starting credential issuance to holder...`); + const registires = await client3.registries().list('issuer'); + await issueCredential(client3, 'issuer', { + ri: registires[0].regk, + s: SCHEMA_SAID, + a: { + i: holderAid['prefix'], + LEI: '5493001KJTIIGC8Y1R17', + }, + }); + console.log(`Issuer sent credential grant to holder.`); + + const grantMsgSaid = await waitAndMarkNotification( + client1, + '/exn/ipex/grant' + ); + console.log( + `Member1 received /exn/ipex/grant msg with SAID: ${grantMsgSaid} ` + ); + const exnRes = await client1.exchanges().get(grantMsgSaid); + + recp = [aid2['state']].map((state) => state['i']); + op1 = await multisigAdmitCredential( + client1, + 'holder', + 'member1', + exnRes.exn.d, + exnRes.exn.i, + recp + ); + console.log( + `Member1 admitted credential with SAID : ${exnRes.exn.e.acdc.d}` + ); + + const grantMsgSaid2 = await waitAndMarkNotification( + client2, + '/exn/ipex/grant' + ); + console.log( + `Member2 received /exn/ipex/grant msg with SAID: ${grantMsgSaid2} ` + ); + const exnRes2 = await client2.exchanges().get(grantMsgSaid2); + + assert.equal(grantMsgSaid, grantMsgSaid2); + + console.log(`Member2 /exn/ipex/grant msg : ` + JSON.stringify(exnRes2)); + + const recp2 = [aid1['state']].map((state) => state['i']); + op2 = await multisigAdmitCredential( + client2, + 'holder', + 'member2', + exnRes.exn.d, + exnRes.exn.i, + recp2 + ); + console.log( + `Member2 admitted credential with SAID : ${exnRes.exn.e.acdc.d}` + ); + + await waitOperation(client1, op1); + await waitOperation(client2, op2); + + let creds1 = await client1.credentials().list(); + console.log(`Member1 has ${creds1.length} credential`); + + const MAX_RETRIES: number = 10; + let retryCount = 0; + while (retryCount < MAX_RETRIES) { + retryCount = retryCount + 1; + console.log(` retry-${retryCount}: No credentials yet...`); + + creds1 = await client1.credentials().list(); + if (creds1.length > 0) break; + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + console.log( + `Member1 has ${creds1.length} credential : ` + JSON.stringify(creds1) + ); + assert.equal(creds1.length, 1); + + await assertOperations(client1, client2, client3); + await warnNotifications(client1, client2, client3); +}, 360000); + +async function createAID(client: SignifyClient, name: string, wits: string[]) { + await getOrCreateIdentifier(client, name); + const aid = await client.identifiers().get(name); + console.log(name, 'AID:', aid.prefix); + return aid; +} + +async function createRegistry( + client: SignifyClient, + name: string, + registryName: string +) { + const result = await client.registries().create({ name, registryName }); + const op = await result.op(); + await waitOperation(client, op); + + const registries = await retry(async () => { + const result = await client.registries().list(name); + assert.equal(result.length, 1); + assert.equal(result[0].name, registryName); + return result; + }); + + return registries[0]; +} + +async function issueCredential( + client: SignifyClient, + name: string, + data: CredentialData +) { + const result = await client.credentials().issue(name, data); + + await waitOperation(client, result.op); + + const creds = await client.credentials().list(); + assert.equal(creds.length, 1); + assert.equal(creds[0].sad.s, data.s); + assert.equal(creds[0].status.s, '0'); + + const dt = createTimestamp(); + + if (data.a.i) { + const [grant, gsigs, end] = await client.ipex().grant({ + senderName: name, + recipient: data.a.i, + datetime: dt, + acdc: result.acdc, + anc: result.anc, + iss: result.iss, + }); + + let op = await client + .ipex() + .submitGrant(name, grant, gsigs, end, [data.a.i]); + op = await waitOperation(client, op); + } + + console.log('Grant message sent'); + + return creds[0]; +} + +function createTimestamp() { + const dt = new Date().toISOString().replace('Z', '000+00:00'); + return dt; +} + +async function multisigAdmitCredential( + client: SignifyClient, + groupName: string, + memberAlias: string, + grantSaid: string, + issuerPrefix: string, + recipients: string[] +): Promise { + const mHab = await client.identifiers().get(memberAlias); + const gHab = await client.identifiers().get(groupName); + + const [admit, sigs, end] = await client.ipex().admit({ + senderName: groupName, + message: '', + grantSaid: grantSaid, + recipient: issuerPrefix, + datetime: TIME, + }); + + const op = await client + .ipex() + .submitAdmit(groupName, admit, sigs, end, [issuerPrefix]); + + const mstate = gHab['state']; + const seal = [ + 'SealEvent', + { i: gHab['prefix'], s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + const sigers = sigs.map((sig: any) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(admit, sigers, seal)); + let atc = ims.substring(admit.size); + atc += end; + const gembeds = { + exn: [admit, atc], + }; + + await client + .exchanges() + .send( + mHab.name, + 'multisig', + mHab, + '/multisig/exn', + { gid: gHab['prefix'] }, + gembeds, + recipients + ); + + return op; +} diff --git a/packages/signify-ts/test-integration/multisig-inception.test.ts b/packages/signify-ts/test-integration/multisig-inception.test.ts new file mode 100644 index 00000000..fa05144a --- /dev/null +++ b/packages/signify-ts/test-integration/multisig-inception.test.ts @@ -0,0 +1,134 @@ +import signify from 'signify-ts'; +import { + getOrCreateClient, + getOrCreateIdentifier, + resolveOobi, + waitForNotifications, + waitOperation, +} from './utils/test-util.ts'; +import { + acceptMultisigIncept, + startMultisigIncept, +} from './utils/multisig-utils.ts'; +import { assert, test } from 'vitest'; +import { step } from './utils/test-step.ts'; + +test('multisig inception', async () => { + await signify.ready(); + const [client1, client2] = await Promise.all([ + getOrCreateClient(), + getOrCreateClient(), + ]); + + const [[aid1], [aid2]] = await Promise.all([ + getOrCreateIdentifier(client1, 'member1'), + getOrCreateIdentifier(client2, 'member2'), + ]); + + await step('Resolve oobis', async () => { + const oobi1 = await client1.oobis().get('member1', 'agent'); + const oobi2 = await client2.oobis().get('member2', 'agent'); + + await Promise.all([ + resolveOobi(client1, oobi2.oobis[0], 'member2'), + resolveOobi(client2, oobi1.oobis[0], 'member1'), + ]); + }); + + await step('Create multisig group', async () => { + const groupName = 'multisig'; + const op1 = await startMultisigIncept(client1, { + groupName, + localMemberName: 'member1', + participants: [aid1, aid2], + toad: 2, + isith: 2, + nsith: 2, + wits: [ + 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM', + 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX', + ], + }); + console.log( + 'Member1 initiated multisig, waiting for others to join...' + ); + + // Second member check notifications and join the multisig + const notifications = await waitForNotifications( + client2, + '/multisig/icp' + ); + await Promise.all( + notifications.map((note) => client2.notifications().mark(note.i)) + ); + const msgSaid = notifications[notifications.length - 1].a.d; + assert(msgSaid, 'msgSaid not defined'); + const op2 = await acceptMultisigIncept(client2, { + localMemberName: 'member2', + groupName, + msgSaid, + }); + console.log('Member2 joined multisig, waiting for others...'); + + // Check for completion + await Promise.all([ + waitOperation(client1, op1), + waitOperation(client2, op2), + ]); + console.log('Multisig created!'); + + const multisig1 = await client1.identifiers().get(groupName); + const multisig2 = await client2.identifiers().get(groupName); + assert.strictEqual(multisig1.prefix, multisig2.prefix); + const members = await client1.identifiers().members(groupName); + assert.strictEqual(members.signing.length, 2); + assert.strictEqual(members.rotation.length, 2); + assert.strictEqual(members.signing[0].aid, aid1); + assert.strictEqual(members.signing[1].aid, aid2); + assert.strictEqual(members.rotation[0].aid, aid1); + assert.strictEqual(members.rotation[1].aid, aid2); + }); + + await step('Test creating another group', async () => { + const groupName = 'multisig2'; + const op1 = await startMultisigIncept(client1, { + groupName, + localMemberName: 'member1', + participants: [aid1, aid2], + toad: 0, + isith: 2, + nsith: 2, + wits: [], + }); + console.log( + 'Member1 initiated multisig, waiting for others to join...' + ); + + // Second member check notifications and join the multisig + const notifications = await waitForNotifications( + client2, + '/multisig/icp' + ); + await Promise.all( + notifications.map((note) => client2.notifications().mark(note.i)) + ); + const msgSaid = notifications[notifications.length - 1].a.d; + assert(msgSaid, 'msgSaid not defined'); + const op2 = await acceptMultisigIncept(client2, { + localMemberName: 'member2', + groupName, + msgSaid, + }); + + await Promise.all([ + waitOperation(client1, op1), + waitOperation(client2, op2), + ]); + + // TODO: https://github.com/WebOfTrust/keria/issues/189 + // const members = await client1.identifiers().members(groupName); + // assert.strictEqual(members.signing.length, 2); + // assert.strictEqual(members.rotating.length, 2); + }); +}, 30000); diff --git a/packages/signify-ts/test-integration/multisig-join.test.ts b/packages/signify-ts/test-integration/multisig-join.test.ts new file mode 100644 index 00000000..9968d893 --- /dev/null +++ b/packages/signify-ts/test-integration/multisig-join.test.ts @@ -0,0 +1,407 @@ +import signify, { Serder, SignifyClient } from 'signify-ts'; +import { + getOrCreateClient, + getOrCreateIdentifier, + markNotification, + waitForNotifications, + waitOperation, +} from './utils/test-util.ts'; +import { assert, beforeAll, describe, test } from 'vitest'; + +describe('multisig-join', () => { + const nameMember1 = 'member1'; + const nameMember2 = 'member2'; + const nameMember3 = 'member3'; + const nameMultisig = 'multisigGroup'; + + let client1: SignifyClient; + let client2: SignifyClient; + let client3: SignifyClient; + + beforeAll(async () => { + await signify.ready(); + + [client1, client2] = await Promise.all([ + getOrCreateClient(), + getOrCreateClient(), + ]); + + await Promise.all([ + createAID(client1, nameMember1, []), + createAID(client2, nameMember2, []), + ]); + + const [oobi1, oobi2] = await Promise.all([ + client1.oobis().get(nameMember1, 'agent'), + client2.oobis().get(nameMember2, 'agent'), + ]); + + const opOobi1 = await client1 + .oobis() + .resolve(oobi2.oobis[0], nameMember2); + const opOobi2 = await client2 + .oobis() + .resolve(oobi1.oobis[0], nameMember1); + await Promise.all([ + waitOperation(client1, opOobi1), + waitOperation(client2, opOobi2), + ]); + }); + + test('should create multisig', async () => { + const [aid1, aid2] = await Promise.all([ + client1.identifiers().get(nameMember1), + client2.identifiers().get(nameMember2), + ]); + const states = [aid1.state, aid2.state]; + const icpResult = await client1.identifiers().create(nameMultisig, { + algo: signify.Algos.group, + mhab: aid1, + isith: 1, + nsith: 1, + toad: aid1.state.b.length, + wits: aid1.state.b, + states: states, + rstates: states, + }); + + const createMultisig1 = await icpResult.op(); + + const serder = icpResult.serder; + + const sigs = icpResult.sigs; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + const ims = signify.d(signify.messagize(serder, sigers)); + const atc = ims.substring(serder.size); + const embeds = { + icp: [serder, atc], + }; + + const smids = [aid2.state.i]; + const recipients = [aid2.state.i]; + + await client1 + .exchanges() + .send( + nameMember1, + nameMultisig, + aid1, + '/multisig/icp', + { gid: serder.pre, smids: smids, rmids: smids }, + embeds, + recipients + ); + + const msgSaid = await waitAndMarkNotification(client2, '/multisig/icp'); + + const response = await client2.groups().getRequest(msgSaid); + const icp = response[0].exn.e.icp; + + const icpResult2 = await client2.identifiers().create(nameMultisig, { + algo: signify.Algos.group, + mhab: aid2, + isith: icp.kt, + nsith: icp.nt, + toad: parseInt(icp.bt), + wits: icp.b, + states, + rstates: states, + }); + const createMultisig2 = await icpResult2.op(); + + const [createResult1, createResult2] = await Promise.all([ + waitOperation(client1, createMultisig1), + waitOperation(client2, createMultisig2), + ]); + + assert.equal(createResult1.response.k[0], aid1.state.k[0]); + assert.equal(createResult1.response.k[1], aid2.state.k[0]); + assert.equal(createResult2.response.k[0], aid1.state.k[0]); + assert.equal(createResult2.response.k[1], aid2.state.k[0]); + + const members1 = await client1.identifiers().members(nameMultisig); + const members2 = await client2.identifiers().members(nameMultisig); + + const agentEnds1 = members1.signing[0].ends.agent; + if (!agentEnds1) { + throw new Error( + 'members1.signing[0].ends.agent is null or undefined' + ); + } + const eid1 = Object.keys(agentEnds1)[0]; + + const agentEnds2 = members2.signing[1].ends.agent; + if (!agentEnds2) { + throw new Error( + 'members2.signing[1].ends.agent is null or undefined' + ); + } + const eid2 = Object.keys(agentEnds2)[0]; + + const [endRoleOperation1, endRoleOperation2] = await Promise.all([ + client1.identifiers().addEndRole(nameMultisig, 'agent', eid1), + client2.identifiers().addEndRole(nameMultisig, 'agent', eid2), + ]); + + await waitOperation(client1, await endRoleOperation1.op()); + await waitOperation(client2, await endRoleOperation2.op()); + }); + + test('should add member3 to multisig', async () => { + client3 = await getOrCreateClient(); + + const aid3 = await createAID(client3, nameMember3, []); + + const [oobi1, oobi2, oobi3, oobi4] = await Promise.all([ + client1.oobis().get(nameMember1, 'agent'), + client2.oobis().get(nameMember2, 'agent'), + client3.oobis().get(nameMember3, 'agent'), + client1.oobis().get(nameMultisig, 'agent'), + ]); + + const oobiMultisig = oobi4.oobis[0].split('/agent/')[0]; + + const [opOobi1, opOobi2, opOobi3, opOobi4, opOobi5] = await Promise.all( + [ + client1.oobis().resolve(oobi3.oobis[0], nameMember3), + client2.oobis().resolve(oobi3.oobis[0], nameMember3), + client3.oobis().resolve(oobi1.oobis[0], nameMember1), + client3.oobis().resolve(oobi2.oobis[0], nameMember2), + client3.oobis().resolve(oobiMultisig, nameMultisig), + ] + ); + await Promise.all([ + waitOperation(client1, opOobi1), + waitOperation(client2, opOobi2), + waitOperation(client3, opOobi3), + waitOperation(client3, opOobi4), + waitOperation(client3, opOobi5), + ]); + + // rotate single sig + const [rotateResult1, rotateResult2] = await Promise.all([ + client1.identifiers().rotate(nameMember1), + client2.identifiers().rotate(nameMember2), + ]); + + await Promise.all([ + waitOperation(client1, await rotateResult1.op()), + waitOperation(client2, await rotateResult2.op()), + ]); + + const [aid1, aid2] = await Promise.all([ + client1.identifiers().get(nameMember1), + client2.identifiers().get(nameMember2), + ]); + + const updates = await Promise.all([ + await client1.keyStates().query(aid2.prefix, '1'), + await client1.keyStates().query(aid3.prefix, '0'), + await client2.keyStates().query(aid1.prefix, '1'), + await client2.keyStates().query(aid3.prefix, '0'), + await client3.keyStates().query(aid1.prefix, '1'), + await client3.keyStates().query(aid2.prefix, '1'), + ]); + + const [aid2State, aid3State, aid1State] = await Promise.all([ + waitOperation(client1, updates[0]), + waitOperation(client1, updates[1]), + waitOperation(client2, updates[2]), + waitOperation(client2, updates[3]), + waitOperation(client3, updates[4]), + waitOperation(client3, updates[5]), + ]); + + const states = [aid1State.response, aid2State.response]; + const rstates = [...states, aid3State.response]; + const rotateOperation1 = await client1 + .identifiers() + .rotate(nameMultisig, { states, rstates }); + + const serder1 = rotateOperation1.serder; + const sigers = rotateOperation1.sigs.map( + (sig) => new signify.Siger({ qb64: sig }) + ); + const ims = signify.d(signify.messagize(serder1, sigers)); + const atc = ims.substring(serder1.size); + const rembeds = { + rot: [serder1, atc], + }; + const smids = states.map((state) => state['i']); + const rmids = rstates.map((state) => state['i']); + const recp = [aid2.state, aid3.state].map((state) => state['i']); + + await client1 + .exchanges() + .send( + nameMember1, + nameMultisig, + aid1, + '/multisig/rot', + { gid: serder1.pre, smids, rmids }, + rembeds, + recp + ); + + await Promise.all([ + waitAndMarkNotification(client2, '/multisig/rot'), + waitAndMarkNotification(client3, '/multisig/rot'), + ]); + + const multisigAid = await client1.identifiers().get(nameMultisig); + + assert.equal(multisigAid.state.k.length, 2); + assert.equal(multisigAid.state.k[0], aid1.state.k[0]); + assert.equal(multisigAid.state.k[1], aid2.state.k[0]); + + assert.equal(multisigAid.state.n.length, 3); + assert.equal(multisigAid.state.n[0], aid1.state.n[0]); + assert.equal(multisigAid.state.n[1], aid2.state.n[0]); + assert.equal(multisigAid.state.n[2], aid3.state.n[0]); + }); + test('Rotate again to get aid3 to current signing keys and join', async () => { + const [rotateResult1, rotateResult2, rotateResult3] = await Promise.all( + [ + client1.identifiers().rotate(nameMember1), + client2.identifiers().rotate(nameMember2), + client3.identifiers().rotate(nameMember3), + ] + ); + + await Promise.all([ + waitOperation(client1, await rotateResult1.op()), + waitOperation(client2, await rotateResult2.op()), + waitOperation(client3, await rotateResult3.op()), + ]); + + const [aid1, aid2, aid3] = await Promise.all([ + client1.identifiers().get(nameMember1), + client2.identifiers().get(nameMember2), + client3.identifiers().get(nameMember3), + ]); + + const updates = await Promise.all([ + await client1.keyStates().query(aid2.prefix, '2'), + await client1.keyStates().query(aid3.prefix, '1'), + await client2.keyStates().query(aid1.prefix, '2'), + await client2.keyStates().query(aid3.prefix, '1'), + await client3.keyStates().query(aid1.prefix, '2'), + await client3.keyStates().query(aid2.prefix, '2'), + ]); + + const [aid2State, aid3State, aid1State] = await Promise.all([ + waitOperation(client1, updates[0]), + waitOperation(client1, updates[1]), + waitOperation(client2, updates[2]), + waitOperation(client2, updates[3]), + waitOperation(client3, updates[4]), + waitOperation(client3, updates[5]), + ]); + + const states = [ + aid1State.response, + aid2State.response, + aid3State.response, + ]; + const rotateOperation1 = await client1 + .identifiers() + .rotate(nameMultisig, { states, rstates: states }); + + const serder1 = rotateOperation1.serder; + const sigers = rotateOperation1.sigs.map( + (sig) => new signify.Siger({ qb64: sig }) + ); + const ims = signify.d(signify.messagize(serder1, sigers)); + const atc = ims.substring(serder1.size); + const rembeds = { + rot: [serder1, atc], + }; + const smids = states.map((state) => state['i']); + const rmids = states.map((state) => state['i']); + const recp = [aid2.state, aid3.state].map((state) => state['i']); + + await client1 + .exchanges() + .send( + nameMember1, + 'multisig', + aid1, + '/multisig/rot', + { gid: serder1.pre, smids, rmids }, + rembeds, + recp + ); + + const rotationNotification3 = await waitAndMarkNotification( + client3, + '/multisig/rot' + ); + + const response = await client3 + .groups() + .getRequest(rotationNotification3); + + const exn3 = response[0].exn; + const serder3 = new Serder(exn3.e.rot); + const keeper3 = await client3.manager!.get(aid3); + const sigs3 = keeper3.sign(signify.b(serder3.raw)); + + const joinOperation = await client3 + .groups() + .join(nameMultisig, serder3, sigs3, exn3.a.gid, smids, rmids); + + await waitOperation(client3, joinOperation); + + const multisigAid = await client3.identifiers().get(nameMultisig); + + assert.equal(multisigAid.state.k.length, 3); + assert.equal(multisigAid.state.k[0], aid1.state.k[0]); + assert.equal(multisigAid.state.k[1], aid2.state.k[0]); + assert.equal(multisigAid.state.k[2], aid3.state.k[0]); + + assert.equal(multisigAid.state.n.length, 3); + assert.equal(multisigAid.state.n[0], aid1.state.n[0]); + assert.equal(multisigAid.state.n[1], aid2.state.n[0]); + assert.equal(multisigAid.state.n[2], aid3.state.n[0]); + + const members = await client3.identifiers().members(nameMultisig); + const agentEnds = members.signing[2].ends.agent; + if (!agentEnds) { + throw new Error( + 'members.signing[2].ends.agent is null or undefined' + ); + } + const eid = Object.keys(agentEnds)[0]; + const endRoleOperation = await client3 + .identifiers() + .addEndRole(nameMultisig, 'agent', eid); + const endRoleResult = await waitOperation( + client3, + await endRoleOperation.op() + ); + + assert.equal(endRoleResult.done, true); + assert.equal(endRoleResult.error, null); + }); +}); + +async function createAID(client: SignifyClient, name: string, wits: string[]) { + await getOrCreateIdentifier(client, name, { + wits: wits, + toad: wits.length, + }); + return await client.identifiers().get(name); +} + +async function waitAndMarkNotification(client: SignifyClient, route: string) { + const notes = await waitForNotifications(client, route); + + await Promise.all( + notes.map(async (note) => { + await markNotification(client, note); + }) + ); + + return notes[notes.length - 1]?.a.d ?? ''; +} diff --git a/packages/signify-ts/test-integration/multisig-vlei-issuance.test.ts b/packages/signify-ts/test-integration/multisig-vlei-issuance.test.ts new file mode 100644 index 00000000..603e5f4b --- /dev/null +++ b/packages/signify-ts/test-integration/multisig-vlei-issuance.test.ts @@ -0,0 +1,1326 @@ +import { assert, test } from 'vitest'; +import signify, { + Saider, + CredentialSubject, + CredentialData, + CreateIdentiferArgs, + randomNonce, + Salter, + HabState, +} from 'signify-ts'; +import { resolveEnvironment } from './utils/resolve-env.ts'; +import { + resolveOobi, + waitOperation, + getOrCreateAID, + getOrCreateClients, + getOrCreateContact, + createTimestamp, + getIssuedCredential, + getReceivedCredential, + waitForCredential, + admitSinglesig, + waitAndMarkNotification, +} from './utils/test-util.ts'; +import { + addEndRoleMultisig, + admitMultisig, + createAIDMultisig, + createRegistryMultisig, + delegateMultisig, + grantMultisig, + issueCredentialMultisig, +} from './utils/multisig-utils.ts'; +import { retry } from './utils/retry.ts'; +import { re } from 'mathjs'; + +const { vleiServerUrl, witnessIds } = resolveEnvironment(); + +const QVI_SCHEMA_SAID = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; +const LE_SCHEMA_SAID = 'ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY'; +const ECR_SCHEMA_SAID = 'EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw'; + +const vLEIServerHostUrl = `${vleiServerUrl}/oobi`; +const QVI_SCHEMA_URL = `${vLEIServerHostUrl}/${QVI_SCHEMA_SAID}`; +const LE_SCHEMA_URL = `${vLEIServerHostUrl}/${LE_SCHEMA_SAID}`; +const ECR_SCHEMA_URL = `${vLEIServerHostUrl}/${ECR_SCHEMA_SAID}`; + +const qviData = { + LEI: '254900OPPU84GM83MG36', +}; + +const leData = { + LEI: '875500ELOZEL05BVXV37', +}; + +const ecrData = { + LEI: leData.LEI, + personLegalName: 'John Doe', + engagementContextRole: 'EBA Submitter', +}; + +const LE_RULES = Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, +})[1]; + +const ECR_RULES = Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, + privacyDisclaimer: { + l: 'It is the sole responsibility of Holders as Issuees of an ECR vLEI Credential to present that Credential in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification.', + }, +})[1]; + +test('multisig-vlei-issuance', async function run() { + /** + * The abbreviations used in this script follows GLEIF vLEI + * ecosystem governance framework (EGF). + * GEDA: GLEIF External Delegated AID + * QVI: Qualified vLEI Issuer + * LE: Legal Entity + * GAR: GLEIF Authorized Representative + * QAR: Qualified vLEI Issuer Authorized Representative + * LAR: Legal Entity Authorized Representative + * ECR: Engagement Context Role Person + */ + + const [ + clientGAR1, + clientGAR2, + clientQAR1, + clientQAR2, + clientQAR3, + clientLAR1, + clientLAR2, + clientLAR3, + clientECR, + ] = await getOrCreateClients(9); + + const kargsAID = { + toad: witnessIds.length, + wits: witnessIds, + }; + const [ + aidGAR1, + aidGAR2, + aidQAR1, + aidQAR2, + aidQAR3, + aidLAR1, + aidLAR2, + aidLAR3, + aidECR, + ] = await Promise.all([ + getOrCreateAID(clientGAR1, 'GAR1', kargsAID), + getOrCreateAID(clientGAR2, 'GAR2', kargsAID), + getOrCreateAID(clientQAR1, 'QAR1', kargsAID), + getOrCreateAID(clientQAR2, 'QAR2', kargsAID), + getOrCreateAID(clientQAR3, 'QAR3', kargsAID), + getOrCreateAID(clientLAR1, 'LAR1', kargsAID), + getOrCreateAID(clientLAR2, 'LAR2', kargsAID), + getOrCreateAID(clientLAR3, 'LAR3', kargsAID), + getOrCreateAID(clientECR, 'ECR', kargsAID), + ]); + + const [ + oobiGAR1, + oobiGAR2, + oobiQAR1, + oobiQAR2, + oobiQAR3, + oobiLAR1, + oobiLAR2, + oobiLAR3, + oobiECR, + ] = await Promise.all([ + clientGAR1.oobis().get('GAR1', 'agent'), + clientGAR2.oobis().get('GAR2', 'agent'), + clientQAR1.oobis().get('QAR1', 'agent'), + clientQAR2.oobis().get('QAR2', 'agent'), + clientQAR3.oobis().get('QAR3', 'agent'), + clientLAR1.oobis().get('LAR1', 'agent'), + clientLAR2.oobis().get('LAR2', 'agent'), + clientLAR3.oobis().get('LAR3', 'agent'), + clientECR.oobis().get('ECR', 'agent'), + ]); + + await Promise.all([ + getOrCreateContact(clientGAR1, 'GAR2', oobiGAR2.oobis[0]), + getOrCreateContact(clientGAR2, 'GAR1', oobiGAR1.oobis[0]), + getOrCreateContact(clientQAR1, 'QAR2', oobiQAR2.oobis[0]), + getOrCreateContact(clientQAR1, 'QAR3', oobiQAR3.oobis[0]), + getOrCreateContact(clientQAR2, 'QAR1', oobiQAR1.oobis[0]), + getOrCreateContact(clientQAR2, 'QAR3', oobiQAR3.oobis[0]), + getOrCreateContact(clientQAR3, 'QAR1', oobiQAR1.oobis[0]), + getOrCreateContact(clientQAR3, 'QAR2', oobiQAR2.oobis[0]), + getOrCreateContact(clientLAR1, 'LAR2', oobiLAR2.oobis[0]), + getOrCreateContact(clientLAR1, 'LAR3', oobiLAR3.oobis[0]), + getOrCreateContact(clientLAR2, 'LAR1', oobiLAR1.oobis[0]), + getOrCreateContact(clientLAR2, 'LAR3', oobiLAR3.oobis[0]), + getOrCreateContact(clientLAR3, 'LAR1', oobiLAR1.oobis[0]), + getOrCreateContact(clientLAR3, 'LAR2', oobiLAR2.oobis[0]), + getOrCreateContact(clientLAR1, 'ECR', oobiECR.oobis[0]), + getOrCreateContact(clientLAR2, 'ECR', oobiECR.oobis[0]), + getOrCreateContact(clientLAR3, 'ECR', oobiECR.oobis[0]), + ]); + + await Promise.all([ + resolveOobi(clientGAR1, QVI_SCHEMA_URL), + resolveOobi(clientGAR2, QVI_SCHEMA_URL), + resolveOobi(clientQAR1, QVI_SCHEMA_URL), + resolveOobi(clientQAR1, LE_SCHEMA_URL), + resolveOobi(clientQAR2, QVI_SCHEMA_URL), + resolveOobi(clientQAR2, LE_SCHEMA_URL), + resolveOobi(clientQAR3, QVI_SCHEMA_URL), + resolveOobi(clientQAR3, LE_SCHEMA_URL), + resolveOobi(clientLAR1, QVI_SCHEMA_URL), + resolveOobi(clientLAR1, LE_SCHEMA_URL), + resolveOobi(clientLAR1, ECR_SCHEMA_URL), + resolveOobi(clientLAR2, QVI_SCHEMA_URL), + resolveOobi(clientLAR2, LE_SCHEMA_URL), + resolveOobi(clientLAR2, ECR_SCHEMA_URL), + resolveOobi(clientLAR3, QVI_SCHEMA_URL), + resolveOobi(clientLAR3, LE_SCHEMA_URL), + resolveOobi(clientLAR3, ECR_SCHEMA_URL), + resolveOobi(clientECR, QVI_SCHEMA_URL), + resolveOobi(clientECR, LE_SCHEMA_URL), + resolveOobi(clientECR, ECR_SCHEMA_URL), + ]); + + // Create a multisig AID for the GEDA. + // Skip if a GEDA AID has already been incepted. + let aidGEDAbyGAR1, aidGEDAbyGAR2: HabState; + try { + aidGEDAbyGAR1 = await clientGAR1.identifiers().get('GEDA'); + aidGEDAbyGAR2 = await clientGAR2.identifiers().get('GEDA'); + } catch { + const rstates = [aidGAR1.state, aidGAR2.state]; + const states = rstates; + + const kargsMultisigAID: CreateIdentiferArgs = { + algo: signify.Algos.group, + isith: ['1/2', '1/2'], + nsith: ['1/2', '1/2'], + toad: kargsAID.toad, + wits: kargsAID.wits, + states: states, + rstates: rstates, + }; + + kargsMultisigAID.mhab = aidGAR1; + const multisigAIDOp1 = await createAIDMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + 'GEDA', + kargsMultisigAID, + true + ); + kargsMultisigAID.mhab = aidGAR2; + const multisigAIDOp2 = await createAIDMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + 'GEDA', + kargsMultisigAID + ); + + await Promise.all([ + waitOperation(clientGAR1, multisigAIDOp1), + waitOperation(clientGAR2, multisigAIDOp2), + ]); + + await waitAndMarkNotification(clientGAR1, '/multisig/icp'); + + aidGEDAbyGAR1 = await clientGAR1.identifiers().get('GEDA'); + aidGEDAbyGAR2 = await clientGAR2.identifiers().get('GEDA'); + } + assert.equal(aidGEDAbyGAR1.prefix, aidGEDAbyGAR2.prefix); + assert.equal(aidGEDAbyGAR1.name, aidGEDAbyGAR2.name); + const aidGEDA = aidGEDAbyGAR1; + + // Add endpoint role authorization for all GARs' agents. + // Skip if they have already been authorized. + let [oobiGEDAbyGAR1, oobiGEDAbyGAR2] = await Promise.all([ + clientGAR1.oobis().get(aidGEDA.name, 'agent'), + clientGAR2.oobis().get(aidGEDA.name, 'agent'), + ]); + if (oobiGEDAbyGAR1.oobis.length == 0 || oobiGEDAbyGAR2.oobis.length == 0) { + const timestamp = createTimestamp(); + const opList1 = await addEndRoleMultisig( + clientGAR1, + aidGEDA.name, + aidGAR1, + [aidGAR2], + aidGEDA, + timestamp, + true + ); + const opList2 = await addEndRoleMultisig( + clientGAR2, + aidGEDA.name, + aidGAR2, + [aidGAR1], + aidGEDA, + timestamp + ); + + await Promise.all(opList1.map((op) => waitOperation(clientGAR1, op))); + await Promise.all(opList2.map((op) => waitOperation(clientGAR2, op))); + + await waitAndMarkNotification(clientGAR1, '/multisig/rpy'); + + [oobiGEDAbyGAR1, oobiGEDAbyGAR2] = await Promise.all([ + clientGAR1.oobis().get(aidGEDA.name, 'agent'), + clientGAR2.oobis().get(aidGEDA.name, 'agent'), + ]); + } + assert.equal(oobiGEDAbyGAR1.role, oobiGEDAbyGAR2.role); + assert.equal(oobiGEDAbyGAR1.oobis[0], oobiGEDAbyGAR2.oobis[0]); + + // QARs, LARs, ECR resolve GEDA's OOBI + const oobiGEDA = oobiGEDAbyGAR1.oobis[0].split('/agent/')[0]; + await Promise.all([ + getOrCreateContact(clientQAR1, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientQAR2, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientQAR3, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientLAR1, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientLAR2, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientLAR3, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientECR, aidGEDA.name, oobiGEDA), + ]); + + // Create a multisig AID for the QVI. + // Skip if a QVI AID has already been incepted. + let aidQVIbyQAR1, aidQVIbyQAR2, aidQVIbyQAR3: HabState; + try { + aidQVIbyQAR1 = await clientQAR1.identifiers().get('QVI'); + aidQVIbyQAR2 = await clientQAR2.identifiers().get('QVI'); + aidQVIbyQAR3 = await clientQAR3.identifiers().get('QVI'); + } catch { + const rstates = [aidQAR1.state, aidQAR2.state, aidQAR3.state]; + const states = rstates; + + const kargsMultisigAID: CreateIdentiferArgs = { + algo: signify.Algos.group, + isith: ['2/3', '1/2', '1/2'], + nsith: ['2/3', '1/2', '1/2'], + toad: kargsAID.toad, + wits: kargsAID.wits, + states: states, + rstates: rstates, + delpre: aidGEDA.prefix, + }; + + kargsMultisigAID.mhab = aidQAR1; + const multisigAIDOp1 = await createAIDMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + 'QVI', + kargsMultisigAID, + true + ); + kargsMultisigAID.mhab = aidQAR2; + const multisigAIDOp2 = await createAIDMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + 'QVI', + kargsMultisigAID + ); + kargsMultisigAID.mhab = aidQAR3; + const multisigAIDOp3 = await createAIDMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + 'QVI', + kargsMultisigAID + ); + + const aidQVIPrefix = multisigAIDOp1.name.split('.')[1]; + assert.equal(multisigAIDOp2.name.split('.')[1], aidQVIPrefix); + assert.equal(multisigAIDOp3.name.split('.')[1], aidQVIPrefix); + + // GEDA anchors delegation with an interaction event. + const anchor = { + i: aidQVIPrefix, + s: '0', + d: aidQVIPrefix, + }; + const ixnOp1 = await delegateMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA, + anchor, + true + ); + const ixnOp2 = await delegateMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA, + anchor + ); + await Promise.all([ + waitOperation(clientGAR1, ixnOp1), + waitOperation(clientGAR2, ixnOp2), + ]); + + await waitAndMarkNotification(clientGAR1, '/multisig/ixn'); + + // QARs query the GEDA's key state + const queryOp1 = await clientQAR1 + .keyStates() + .query(aidGEDA.prefix, '1'); + const queryOp2 = await clientQAR2 + .keyStates() + .query(aidGEDA.prefix, '1'); + const queryOp3 = await clientQAR3 + .keyStates() + .query(aidGEDA.prefix, '1'); + + await Promise.all([ + waitOperation(clientQAR1, multisigAIDOp1), + waitOperation(clientQAR2, multisigAIDOp2), + waitOperation(clientQAR3, multisigAIDOp3), + waitOperation(clientQAR1, queryOp1), + waitOperation(clientQAR2, queryOp2), + waitOperation(clientQAR3, queryOp3), + ]); + + await waitAndMarkNotification(clientQAR1, '/multisig/icp'); + + aidQVIbyQAR1 = await clientQAR1.identifiers().get('QVI'); + aidQVIbyQAR2 = await clientQAR2.identifiers().get('QVI'); + aidQVIbyQAR3 = await clientQAR3.identifiers().get('QVI'); + } + assert.equal(aidQVIbyQAR1.prefix, aidQVIbyQAR2.prefix); + assert.equal(aidQVIbyQAR1.prefix, aidQVIbyQAR3.prefix); + assert.equal(aidQVIbyQAR1.name, aidQVIbyQAR2.name); + assert.equal(aidQVIbyQAR1.name, aidQVIbyQAR3.name); + const aidQVI = aidQVIbyQAR1; + + // Add endpoint role authorization for all QARs' agents. + // Skip if they have already been authorized. + let [oobiQVIbyQAR1, oobiQVIbyQAR2, oobiQVIbyQAR3] = await Promise.all([ + clientQAR1.oobis().get(aidQVI.name, 'agent'), + clientQAR2.oobis().get(aidQVI.name, 'agent'), + clientQAR3.oobis().get(aidQVI.name, 'agent'), + ]); + if ( + oobiQVIbyQAR1.oobis.length == 0 || + oobiQVIbyQAR2.oobis.length == 0 || + oobiQVIbyQAR3.oobis.length == 0 + ) { + const timestamp = createTimestamp(); + const opList1 = await addEndRoleMultisig( + clientQAR1, + aidQVI.name, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI, + timestamp, + true + ); + const opList2 = await addEndRoleMultisig( + clientQAR2, + aidQVI.name, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI, + timestamp + ); + const opList3 = await addEndRoleMultisig( + clientQAR3, + aidQVI.name, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI, + timestamp + ); + + await Promise.all(opList1.map((op) => waitOperation(clientQAR1, op))); + await Promise.all(opList2.map((op) => waitOperation(clientQAR2, op))); + await Promise.all(opList3.map((op) => waitOperation(clientQAR3, op))); + + await waitAndMarkNotification(clientQAR1, '/multisig/rpy'); + await waitAndMarkNotification(clientQAR2, '/multisig/rpy'); + + [oobiQVIbyQAR1, oobiQVIbyQAR2, oobiQVIbyQAR3] = await Promise.all([ + clientQAR1.oobis().get(aidQVI.name, 'agent'), + clientQAR2.oobis().get(aidQVI.name, 'agent'), + clientQAR3.oobis().get(aidQVI.name, 'agent'), + ]); + } + assert.equal(oobiQVIbyQAR1.role, oobiQVIbyQAR2.role); + assert.equal(oobiQVIbyQAR1.role, oobiQVIbyQAR3.role); + assert.equal(oobiQVIbyQAR1.oobis[0], oobiQVIbyQAR2.oobis[0]); + assert.equal(oobiQVIbyQAR1.oobis[0], oobiQVIbyQAR3.oobis[0]); + + // GARs, LARs, ECR resolve QVI AID's OOBI + const oobiQVI = oobiQVIbyQAR1.oobis[0].split('/agent/')[0]; + await Promise.all([ + getOrCreateContact(clientGAR1, aidQVI.name, oobiQVI), + getOrCreateContact(clientGAR2, aidQVI.name, oobiQVI), + getOrCreateContact(clientLAR1, aidQVI.name, oobiQVI), + getOrCreateContact(clientLAR2, aidQVI.name, oobiQVI), + getOrCreateContact(clientLAR3, aidQVI.name, oobiQVI), + getOrCreateContact(clientECR, aidQVI.name, oobiQVI), + ]); + + // GARs creates a registry for GEDA. + // Skip if the registry has already been created. + let [gedaRegistrybyGAR1, gedaRegistrybyGAR2] = await Promise.all([ + clientGAR1.registries().list(aidGEDA.name), + clientGAR2.registries().list(aidGEDA.name), + ]); + if (gedaRegistrybyGAR1.length == 0 && gedaRegistrybyGAR2.length == 0) { + const nonce = randomNonce(); + const registryOp1 = await createRegistryMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA, + 'gedaRegistry', + nonce, + true + ); + const registryOp2 = await createRegistryMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA, + 'gedaRegistry', + nonce + ); + + await Promise.all([ + waitOperation(clientGAR1, registryOp1), + waitOperation(clientGAR2, registryOp2), + ]); + + await waitAndMarkNotification(clientGAR1, '/multisig/vcp'); + + [gedaRegistrybyGAR1, gedaRegistrybyGAR2] = await Promise.all([ + retry(async () => { + const registries = await clientGAR1 + .registries() + .list(aidGEDA.name); + assert(registries.length > 0, 'Registry not found yet'); + return registries; + }), + retry(async () => { + const registries = await clientGAR2 + .registries() + .list(aidGEDA.name); + assert(registries.length > 0, 'Registry not found yet'); + return registries; + }), + ]); + } + assert.equal(gedaRegistrybyGAR1[0].regk, gedaRegistrybyGAR2[0].regk); + assert.equal(gedaRegistrybyGAR1[0].name, gedaRegistrybyGAR2[0].name); + const gedaRegistry = gedaRegistrybyGAR1[0]; + + // GEDA issues a QVI vLEI credential to the QVI AID. + // Skip if the credential has already been issued. + let qviCredbyGAR1 = await getIssuedCredential( + clientGAR1, + aidGEDA, + aidQVI, + QVI_SCHEMA_SAID + ); + let qviCredbyGAR2 = await getIssuedCredential( + clientGAR2, + aidGEDA, + aidQVI, + QVI_SCHEMA_SAID + ); + if (!(qviCredbyGAR1 && qviCredbyGAR2)) { + const kargsSub: CredentialSubject = { + i: aidQVI.prefix, + dt: createTimestamp(), + ...qviData, + }; + const kargsIss: CredentialData = { + i: aidGEDA.prefix, + ri: gedaRegistry.regk, + s: QVI_SCHEMA_SAID, + a: kargsSub, + }; + const IssOp1 = await issueCredentialMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA.name, + kargsIss, + true + ); + const IssOp2 = await issueCredentialMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA.name, + kargsIss + ); + + await Promise.all([ + waitOperation(clientGAR1, IssOp1), + waitOperation(clientGAR2, IssOp2), + ]); + + await waitAndMarkNotification(clientGAR1, '/multisig/iss'); + + qviCredbyGAR1 = await getIssuedCredential( + clientGAR1, + aidGEDA, + aidQVI, + QVI_SCHEMA_SAID + ); + qviCredbyGAR2 = await getIssuedCredential( + clientGAR2, + aidGEDA, + aidQVI, + QVI_SCHEMA_SAID + ); + + const grantTime = createTimestamp(); + await grantMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA, + aidQVI, + qviCredbyGAR1, + grantTime, + true + ); + await grantMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA, + aidQVI, + qviCredbyGAR2, + grantTime + ); + + await waitAndMarkNotification(clientGAR1, '/multisig/exn'); + } + assert.equal(qviCredbyGAR1.sad.d, qviCredbyGAR2.sad.d); + assert.equal(qviCredbyGAR1.sad.s, QVI_SCHEMA_SAID); + assert.equal(qviCredbyGAR1.sad.i, aidGEDA.prefix); + + if ('a' in qviCredbyGAR1.sad && qviCredbyGAR1.sad.a !== undefined) { + assert.equal(qviCredbyGAR1.sad.a.i, aidQVI.prefix); + } else { + throw new Error('Expected qviCredbyGAR1.sad.a to be defined'); + } + + assert.equal(qviCredbyGAR1.status.s, '0'); + assert(qviCredbyGAR1.atc !== undefined); + const qviCred = qviCredbyGAR1; + console.log( + 'GEDA has issued a QVI vLEI credential with SAID:', + qviCred.sad.d + ); + + // GEDA and QVI exchange grant and admit messages. + // Skip if QVI has already received the credential. + let qviCredbyQAR1 = await getReceivedCredential(clientQAR1, qviCred.sad.d); + let qviCredbyQAR2 = await getReceivedCredential(clientQAR2, qviCred.sad.d); + let qviCredbyQAR3 = await getReceivedCredential(clientQAR3, qviCred.sad.d); + if (!(qviCredbyQAR1 && qviCredbyQAR2 && qviCredbyQAR3)) { + const admitTime = createTimestamp(); + await admitMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI, + aidGEDA, + admitTime + ); + await admitMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI, + aidGEDA, + admitTime + ); + await admitMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI, + aidGEDA, + admitTime + ); + await waitAndMarkNotification(clientGAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientGAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR1, '/multisig/exn'); + await waitAndMarkNotification(clientQAR2, '/multisig/exn'); + await waitAndMarkNotification(clientQAR3, '/multisig/exn'); + await waitAndMarkNotification(clientQAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR3, '/exn/ipex/admit'); + + qviCredbyQAR1 = await waitForCredential(clientQAR1, qviCred.sad.d); + qviCredbyQAR2 = await waitForCredential(clientQAR2, qviCred.sad.d); + qviCredbyQAR3 = await waitForCredential(clientQAR3, qviCred.sad.d); + } + assert.equal(qviCred.sad.d, qviCredbyQAR1.sad.d); + assert.equal(qviCred.sad.d, qviCredbyQAR2.sad.d); + assert.equal(qviCred.sad.d, qviCredbyQAR3.sad.d); + + // Create a multisig AID for the LE. + // Skip if a LE AID has already been incepted. + let aidLEbyLAR1, aidLEbyLAR2, aidLEbyLAR3: HabState; + try { + aidLEbyLAR1 = await clientLAR1.identifiers().get('LE'); + aidLEbyLAR2 = await clientLAR2.identifiers().get('LE'); + aidLEbyLAR3 = await clientLAR3.identifiers().get('LE'); + } catch { + const rstates = [aidLAR1.state, aidLAR2.state, aidLAR3.state]; + const states = rstates; + + const kargsMultisigAID: CreateIdentiferArgs = { + algo: signify.Algos.group, + isith: ['2/3', '1/2', '1/2'], + nsith: ['2/3', '1/2', '1/2'], + toad: kargsAID.toad, + wits: kargsAID.wits, + states: states, + rstates: rstates, + }; + + kargsMultisigAID.mhab = aidLAR1; + const multisigAIDOp1 = await createAIDMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + 'LE', + kargsMultisigAID, + true + ); + kargsMultisigAID.mhab = aidLAR2; + const multisigAIDOp2 = await createAIDMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + 'LE', + kargsMultisigAID + ); + kargsMultisigAID.mhab = aidLAR3; + const multisigAIDOp3 = await createAIDMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + 'LE', + kargsMultisigAID + ); + + await Promise.all([ + waitOperation(clientLAR1, multisigAIDOp1), + waitOperation(clientLAR2, multisigAIDOp2), + waitOperation(clientLAR3, multisigAIDOp3), + ]); + + await waitAndMarkNotification(clientLAR1, '/multisig/icp'); + + aidLEbyLAR1 = await clientLAR1.identifiers().get('LE'); + aidLEbyLAR2 = await clientLAR2.identifiers().get('LE'); + aidLEbyLAR3 = await clientLAR3.identifiers().get('LE'); + } + assert.equal(aidLEbyLAR1.prefix, aidLEbyLAR2.prefix); + assert.equal(aidLEbyLAR1.prefix, aidLEbyLAR3.prefix); + assert.equal(aidLEbyLAR1.name, aidLEbyLAR2.name); + assert.equal(aidLEbyLAR1.name, aidLEbyLAR3.name); + const aidLE = aidLEbyLAR1; + + // Add endpoint role authorization for all LARs' agents. + // Skip if they have already been authorized. + let [oobiLEbyLAR1, oobiLEbyLAR2, oobiLEbyLAR3] = await Promise.all([ + clientLAR1.oobis().get(aidLE.name, 'agent'), + clientLAR2.oobis().get(aidLE.name, 'agent'), + clientLAR3.oobis().get(aidLE.name, 'agent'), + ]); + if ( + oobiLEbyLAR1.oobis.length == 0 || + oobiLEbyLAR2.oobis.length == 0 || + oobiLEbyLAR3.oobis.length == 0 + ) { + const timestamp = createTimestamp(); + const opList1 = await addEndRoleMultisig( + clientLAR1, + aidLE.name, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE, + timestamp, + true + ); + const opList2 = await addEndRoleMultisig( + clientLAR2, + aidLE.name, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE, + timestamp + ); + const opList3 = await addEndRoleMultisig( + clientLAR3, + aidLE.name, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE, + timestamp + ); + + await Promise.all(opList1.map((op) => waitOperation(clientLAR1, op))); + await Promise.all(opList2.map((op) => waitOperation(clientLAR2, op))); + await Promise.all(opList3.map((op) => waitOperation(clientLAR3, op))); + + await waitAndMarkNotification(clientLAR1, '/multisig/rpy'); + await waitAndMarkNotification(clientLAR2, '/multisig/rpy'); + + [oobiLEbyLAR1, oobiLEbyLAR2, oobiLEbyLAR3] = await Promise.all([ + clientLAR1.oobis().get(aidLE.name, 'agent'), + clientLAR2.oobis().get(aidLE.name, 'agent'), + clientLAR3.oobis().get(aidLE.name, 'agent'), + ]); + } + assert.equal(oobiLEbyLAR1.role, oobiLEbyLAR2.role); + assert.equal(oobiLEbyLAR1.role, oobiLEbyLAR3.role); + assert.equal(oobiLEbyLAR1.oobis[0], oobiLEbyLAR2.oobis[0]); + assert.equal(oobiLEbyLAR1.oobis[0], oobiLEbyLAR3.oobis[0]); + + // QARs, ECR resolve LE AID's OOBI + const oobiLE = oobiLEbyLAR1.oobis[0].split('/agent/')[0]; + await Promise.all([ + getOrCreateContact(clientQAR1, aidLE.name, oobiLE), + getOrCreateContact(clientQAR2, aidLE.name, oobiLE), + getOrCreateContact(clientQAR3, aidLE.name, oobiLE), + getOrCreateContact(clientECR, aidLE.name, oobiLE), + ]); + + // QARs creates a registry for QVI AID. + // Skip if the registry has already been created. + let [qviRegistrybyQAR1, qviRegistrybyQAR2, qviRegistrybyQAR3] = + await Promise.all([ + clientQAR1.registries().list(aidQVI.name), + clientQAR2.registries().list(aidQVI.name), + clientQAR3.registries().list(aidQVI.name), + ]); + if ( + qviRegistrybyQAR1.length == 0 && + qviRegistrybyQAR2.length == 0 && + qviRegistrybyQAR3.length == 0 + ) { + const nonce = randomNonce(); + const registryOp1 = await createRegistryMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI, + 'qviRegistry', + nonce, + true + ); + const registryOp2 = await createRegistryMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI, + 'qviRegistry', + nonce + ); + const registryOp3 = await createRegistryMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI, + 'qviRegistry', + nonce + ); + + await Promise.all([ + waitOperation(clientQAR1, registryOp1), + waitOperation(clientQAR2, registryOp2), + waitOperation(clientQAR3, registryOp3), + ]); + + await waitAndMarkNotification(clientQAR1, '/multisig/vcp'); + + [qviRegistrybyQAR1, qviRegistrybyQAR2, qviRegistrybyQAR3] = + await Promise.all([ + retry(async () => { + const registries = await clientQAR1 + .registries() + .list(aidQVI.name); + assert(registries.length > 0, 'Registry not found yet'); + return registries; + }), + retry(async () => { + const registries = await clientQAR2 + .registries() + .list(aidQVI.name); + assert(registries.length > 0, 'Registry not found yet'); + return registries; + }), + retry(async () => { + const registries = await clientQAR3 + .registries() + .list(aidQVI.name); + assert(registries.length > 0, 'Registry not found yet'); + return registries; + }), + ]); + } + assert.equal(qviRegistrybyQAR1[0].regk, qviRegistrybyQAR2[0].regk); + assert.equal(qviRegistrybyQAR1[0].regk, qviRegistrybyQAR3[0].regk); + assert.equal(qviRegistrybyQAR1[0].name, qviRegistrybyQAR2[0].name); + assert.equal(qviRegistrybyQAR1[0].name, qviRegistrybyQAR3[0].name); + const qviRegistry = qviRegistrybyQAR1[0]; + + // QVI issues a LE vLEI credential to the LE. + // Skip if the credential has already been issued. + let leCredbyQAR1 = await getIssuedCredential( + clientQAR1, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + let leCredbyQAR2 = await getIssuedCredential( + clientQAR2, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + let leCredbyQAR3 = await getIssuedCredential( + clientQAR3, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + if (!(leCredbyQAR1 && leCredbyQAR2 && leCredbyQAR3)) { + const leCredSource = Saider.saidify({ + d: '', + qvi: { + n: qviCred.sad.d, + s: qviCred.sad.s, + }, + })[1]; + + const kargsSub: CredentialSubject = { + i: aidLE.prefix, + dt: createTimestamp(), + ...leData, + }; + const kargsIss: CredentialData = { + i: aidQVI.prefix, + ri: qviRegistry.regk, + s: LE_SCHEMA_SAID, + a: kargsSub, + e: leCredSource, + r: LE_RULES, + }; + const IssOp1 = await issueCredentialMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI.name, + kargsIss, + true + ); + const IssOp2 = await issueCredentialMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI.name, + kargsIss + ); + const IssOp3 = await issueCredentialMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI.name, + kargsIss + ); + + await Promise.all([ + waitOperation(clientQAR1, IssOp1), + waitOperation(clientQAR2, IssOp2), + waitOperation(clientQAR3, IssOp3), + ]); + + await waitAndMarkNotification(clientQAR1, '/multisig/iss'); + + leCredbyQAR1 = await getIssuedCredential( + clientQAR1, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + leCredbyQAR2 = await getIssuedCredential( + clientQAR2, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + leCredbyQAR3 = await getIssuedCredential( + clientQAR3, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + + const grantTime = createTimestamp(); + await grantMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI, + aidLE, + leCredbyQAR1, + grantTime, + true + ); + await grantMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI, + aidLE, + leCredbyQAR2, + grantTime + ); + await grantMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI, + aidLE, + leCredbyQAR3, + grantTime + ); + + await waitAndMarkNotification(clientQAR1, '/multisig/exn'); + } + assert.equal(leCredbyQAR1.sad.d, leCredbyQAR2.sad.d); + assert.equal(leCredbyQAR1.sad.d, leCredbyQAR3.sad.d); + assert.equal(leCredbyQAR1.sad.s, LE_SCHEMA_SAID); + assert.equal(leCredbyQAR1.sad.i, aidQVI.prefix); + + if ('a' in leCredbyQAR1.sad && leCredbyQAR1.sad.a !== undefined) { + assert.equal(leCredbyQAR1.sad.a.i, aidLE.prefix); + } else { + throw new Error('Expected leCredbyQAR1.sad.a to be defined'); + } + + assert.equal(leCredbyQAR1.status.s, '0'); + assert(leCredbyQAR1.atc !== undefined); + const leCred = leCredbyQAR1; + console.log('QVI has issued a LE vLEI credential with SAID:', leCred.sad.d); + + // QVI and LE exchange grant and admit messages. + // Skip if LE has already received the credential. + let leCredbyLAR1 = await getReceivedCredential(clientLAR1, leCred.sad.d); + let leCredbyLAR2 = await getReceivedCredential(clientLAR2, leCred.sad.d); + let leCredbyLAR3 = await getReceivedCredential(clientLAR3, leCred.sad.d); + if (!(leCredbyLAR1 && leCredbyLAR2 && leCredbyLAR3)) { + const admitTime = createTimestamp(); + await admitMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE, + aidQVI, + admitTime + ); + await admitMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE, + aidQVI, + admitTime + ); + await admitMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE, + aidQVI, + admitTime + ); + await waitAndMarkNotification(clientQAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR3, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR1, '/multisig/exn'); + await waitAndMarkNotification(clientLAR2, '/multisig/exn'); + await waitAndMarkNotification(clientLAR3, '/multisig/exn'); + await waitAndMarkNotification(clientLAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR3, '/exn/ipex/admit'); + + leCredbyLAR1 = await waitForCredential(clientLAR1, leCred.sad.d); + leCredbyLAR2 = await waitForCredential(clientLAR2, leCred.sad.d); + leCredbyLAR3 = await waitForCredential(clientLAR3, leCred.sad.d); + } + assert.equal(leCred.sad.d, leCredbyLAR1.sad.d); + assert.equal(leCred.sad.d, leCredbyLAR2.sad.d); + assert.equal(leCred.sad.d, leCredbyLAR3.sad.d); + + // LARs creates a registry for LE AID. + // Skip if the registry has already been created. + let [leRegistrybyLAR1, leRegistrybyLAR2, leRegistrybyLAR3] = + await Promise.all([ + clientLAR1.registries().list(aidLE.name), + clientLAR2.registries().list(aidLE.name), + clientLAR3.registries().list(aidLE.name), + ]); + if ( + leRegistrybyLAR1.length == 0 && + leRegistrybyLAR2.length == 0 && + leRegistrybyLAR3.length == 0 + ) { + const nonce = randomNonce(); + const registryOp1 = await createRegistryMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE, + 'leRegistry', + nonce, + true + ); + const registryOp2 = await createRegistryMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE, + 'leRegistry', + nonce + ); + const registryOp3 = await createRegistryMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE, + 'leRegistry', + nonce + ); + + await Promise.all([ + waitOperation(clientLAR1, registryOp1), + waitOperation(clientLAR2, registryOp2), + waitOperation(clientLAR3, registryOp3), + ]); + + await waitAndMarkNotification(clientLAR1, '/multisig/vcp'); + + [leRegistrybyLAR1, leRegistrybyLAR2, leRegistrybyLAR3] = + await Promise.all([ + retry(async () => { + const registries = await clientLAR1 + .registries() + .list(aidLE.name); + assert(registries.length > 0, 'Registry not found yet'); + return registries; + }), + retry(async () => { + const registries = await clientLAR2 + .registries() + .list(aidLE.name); + assert(registries.length > 0, 'Registry not found yet'); + return registries; + }), + retry(async () => { + const registries = await clientLAR3 + .registries() + .list(aidLE.name); + assert(registries.length > 0, 'Registry not found yet'); + return registries; + }), + ]); + } + assert.equal(leRegistrybyLAR1[0].regk, leRegistrybyLAR2[0].regk); + assert.equal(leRegistrybyLAR1[0].regk, leRegistrybyLAR3[0].regk); + assert.equal(leRegistrybyLAR1[0].name, leRegistrybyLAR2[0].name); + assert.equal(leRegistrybyLAR1[0].name, leRegistrybyLAR3[0].name); + const leRegistry = leRegistrybyLAR1[0]; + + // LE issues a ECR vLEI credential to the ECR Person. + // Skip if the credential has already been issued. + let ecrCredbyLAR1 = await getIssuedCredential( + clientLAR1, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + let ecrCredbyLAR2 = await getIssuedCredential( + clientLAR2, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + let ecrCredbyLAR3 = await getIssuedCredential( + clientLAR3, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + if (!(ecrCredbyLAR1 && ecrCredbyLAR2 && ecrCredbyLAR3)) { + console.log('Issuing ECR vLEI Credential from LE'); + const ecrCredSource = Saider.saidify({ + d: '', + le: { + n: leCred.sad.d, + s: leCred.sad.s, + }, + })[1]; + + const kargsSub: CredentialSubject = { + i: aidECR.prefix, + dt: createTimestamp(), + u: new Salter({}).qb64, + ...ecrData, + }; + const kargsIss: CredentialData = { + u: new Salter({}).qb64, + i: aidLE.prefix, + ri: leRegistry.regk, + s: ECR_SCHEMA_SAID, + a: kargsSub, + e: ecrCredSource, + r: ECR_RULES, + }; + + const IssOp1 = await issueCredentialMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE.name, + kargsIss, + true + ); + const IssOp2 = await issueCredentialMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE.name, + kargsIss + ); + const IssOp3 = await issueCredentialMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE.name, + kargsIss + ); + + await Promise.all([ + waitOperation(clientLAR1, IssOp1), + waitOperation(clientLAR2, IssOp2), + waitOperation(clientLAR3, IssOp3), + ]); + + await waitAndMarkNotification(clientLAR1, '/multisig/iss'); + + ecrCredbyLAR1 = await getIssuedCredential( + clientLAR1, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + ecrCredbyLAR2 = await getIssuedCredential( + clientLAR2, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + ecrCredbyLAR3 = await getIssuedCredential( + clientLAR3, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + + const grantTime = createTimestamp(); + await grantMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE, + aidECR, + ecrCredbyLAR1, + grantTime, + true + ); + await grantMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE, + aidECR, + ecrCredbyLAR2, + grantTime + ); + await grantMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE, + aidECR, + ecrCredbyLAR3, + grantTime + ); + + await waitAndMarkNotification(clientLAR1, '/multisig/exn'); + } + assert.equal(ecrCredbyLAR1.sad.d, ecrCredbyLAR2.sad.d); + assert.equal(ecrCredbyLAR1.sad.d, ecrCredbyLAR3.sad.d); + assert.equal(ecrCredbyLAR1.sad.s, ECR_SCHEMA_SAID); + assert.equal(ecrCredbyLAR1.sad.i, aidLE.prefix); + + if ('a' in ecrCredbyLAR1.sad && ecrCredbyLAR1.sad.a !== undefined) { + assert.equal(ecrCredbyLAR1.sad.a.i, aidECR.prefix); + } else { + throw new Error('Expected ecrCredbyLAR1.sad.a to be defined'); + } + + assert.equal(ecrCredbyLAR1.status.s, '0'); + assert(ecrCredbyLAR1.atc !== undefined); + const ecrCred = ecrCredbyLAR1; + console.log( + 'LE has issued an ECR vLEI credential with SAID:', + ecrCred.sad.d + ); + + // LE and ECR Person exchange grant and admit messages. + // Skip if ECR Person has already received the credential. + let ecrCredbyECR = await getReceivedCredential(clientECR, ecrCred.sad.d); + if (!ecrCredbyECR) { + await admitSinglesig(clientECR, aidECR.name, aidLE); + await waitAndMarkNotification(clientLAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR3, '/exn/ipex/admit'); + + ecrCredbyECR = await waitForCredential(clientECR, ecrCred.sad.d); + } + assert.equal(ecrCred.sad.d, ecrCredbyECR.sad.d); +}, 360000); diff --git a/packages/signify-ts/test-integration/multisig.test.ts b/packages/signify-ts/test-integration/multisig.test.ts new file mode 100644 index 00000000..31621058 --- /dev/null +++ b/packages/signify-ts/test-integration/multisig.test.ts @@ -0,0 +1,1270 @@ +import { assert, test } from 'vitest'; +import signify, { + SignifyClient, + Serder, + IssueCredentialResult, +} from 'signify-ts'; +import { resolveEnvironment } from './utils/resolve-env.ts'; +import { + assertOperations, + getOrCreateClient, + getOrCreateIdentifier, + waitAndMarkNotification, + waitOperation, + warnNotifications, +} from './utils/test-util.ts'; + +const { vleiServerUrl } = resolveEnvironment(); +const WITNESS_AIDS = [ + 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM', + 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX', +]; + +const SCHEMA_SAID = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; +const SCHEMA_OOBI = `${vleiServerUrl}/oobi/${SCHEMA_SAID}`; + +test('multisig', async function run() { + await signify.ready(); + // Boot Four clients + const [client1, client2, client3, client4] = await Promise.all([ + getOrCreateClient(), + getOrCreateClient(), + getOrCreateClient(), + getOrCreateClient(), + ]); + + // Create four identifiers, one for each client + let [aid1, aid2, aid3, aid4] = await Promise.all([ + createAID(client1, 'member1', WITNESS_AIDS), + createAID(client2, 'member2', WITNESS_AIDS), + createAID(client3, 'member3', WITNESS_AIDS), + createAID(client4, 'holder', WITNESS_AIDS), + ]); + + // Exchange OOBIs + console.log('Resolving OOBIs'); + const [oobi1, oobi2, oobi3, oobi4] = await Promise.all([ + client1.oobis().get('member1', 'agent'), + client2.oobis().get('member2', 'agent'), + client3.oobis().get('member3', 'agent'), + client4.oobis().get('holder', 'agent'), + ]); + + let op1 = await client1.oobis().resolve(oobi2.oobis[0], 'member2'); + op1 = await waitOperation(client1, op1); + op1 = await client1.oobis().resolve(oobi3.oobis[0], 'member3'); + op1 = await waitOperation(client1, op1); + op1 = await client1.oobis().resolve(SCHEMA_OOBI, 'schema'); + op1 = await waitOperation(client1, op1); + op1 = await client1.oobis().resolve(oobi4.oobis[0], 'holder'); + op1 = await waitOperation(client1, op1); + console.log('Member1 resolved 4 OOBIs'); + + let op2 = await client2.oobis().resolve(oobi1.oobis[0], 'member1'); + op2 = await waitOperation(client2, op2); + op2 = await client2.oobis().resolve(oobi3.oobis[0], 'member3'); + op2 = await waitOperation(client2, op2); + op2 = await client2.oobis().resolve(SCHEMA_OOBI, 'schema'); + op2 = await waitOperation(client2, op2); + op2 = await client2.oobis().resolve(oobi4.oobis[0], 'holder'); + op2 = await waitOperation(client2, op2); + console.log('Member2 resolved 4 OOBIs'); + + let op3 = await client3.oobis().resolve(oobi1.oobis[0], 'member1'); + op3 = await waitOperation(client3, op3); + op3 = await client3.oobis().resolve(oobi2.oobis[0], 'member2'); + op3 = await waitOperation(client3, op3); + op3 = await client3.oobis().resolve(SCHEMA_OOBI, 'schema'); + op3 = await waitOperation(client3, op3); + op3 = await client3.oobis().resolve(oobi4.oobis[0], 'holder'); + op3 = await waitOperation(client3, op3); + console.log('Member3 resolved 4 OOBIs'); + + let op4 = await client4.oobis().resolve(oobi1.oobis[0], 'member1'); + op4 = await waitOperation(client4, op4); + op4 = await client4.oobis().resolve(oobi2.oobis[0], 'member2'); + op4 = await waitOperation(client4, op4); + op4 = await client4.oobis().resolve(oobi3.oobis[0], 'member3'); + op4 = await waitOperation(client4, op4); + + op4 = await client4.oobis().resolve(SCHEMA_OOBI, 'schema'); + op4 = await waitOperation(client4, op4); + + console.log('Holder resolved 4 OOBIs'); + + // First member challenge the other members with a random list of words + // List of words should be passed to the other members out of band + // The other members should do the same challenge/response flow, not shown here for brevity + const words = (await client1.challenges().generate(128)).words; + console.log('Member1 generated challenge words:', words); + + await client2.challenges().respond('member2', aid1.prefix, words); + console.log('Member2 responded challenge with signed words'); + + await client3.challenges().respond('member3', aid1.prefix, words); + console.log('Member3 responded challenge with signed words'); + + op1 = await client1.challenges().verify(aid2.prefix, words); + op1 = await waitOperation(client1, op1); + console.log('Member1 verified challenge response from member2'); + let exnwords = new Serder(op1.response.exn); + op1 = await client1.challenges().responded(aid2.prefix, exnwords.sad.d); + console.log('Member1 marked challenge response as accepted'); + + op1 = await client1.challenges().verify(aid3.prefix, words); + op1 = await waitOperation(client1, op1); + console.log('Member1 verified challenge response from member3'); + exnwords = new Serder(op1.response.exn); + op1 = await client1.challenges().responded(aid3.prefix, exnwords.sad.d); + console.log('Member1 marked challenge response as accepted'); + + // First member start the creation of a multisig identifier + let rstates = [aid1['state'], aid2['state'], aid3['state']]; + let states = rstates; + let icpResult1 = await client1.identifiers().create('multisig', { + algo: signify.Algos.group, + mhab: aid1, + isith: 3, + nsith: 3, + toad: aid1.state.b.length, + wits: aid1.state.b, + states: states, + rstates: rstates, + }); + op1 = await icpResult1.op(); + let serder = icpResult1.serder; + + let sigs = icpResult1.sigs; + let sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + let ims = signify.d(signify.messagize(serder, sigers)); + let atc = ims.substring(serder.size); + let embeds = { + icp: [serder, atc], + }; + + let smids = states.map((state) => state['i']); + let recp = [aid2['state'], aid3['state']].map((state) => state['i']); + + await client1 + .exchanges() + .send( + 'member1', + 'multisig', + aid1, + '/multisig/icp', + { gid: serder.pre, smids: smids, rmids: smids }, + embeds, + recp + ); + console.log('Member1 initiated multisig, waiting for others to join...'); + + // Second member check notifications and join the multisig + + let msgSaid = await waitAndMarkNotification(client2, '/multisig/icp'); + console.log('Member2 received exchange message to join multisig'); + + let res = await client2.groups().getRequest(msgSaid); + let exn = res[0].exn; + let icp = exn.e.icp; + + let icpResult2 = await client2.identifiers().create('multisig', { + algo: signify.Algos.group, + mhab: aid2, + isith: icp.kt, + nsith: icp.nt, + toad: parseInt(icp.bt), + wits: icp.b, + states: states, + rstates: rstates, + }); + op2 = await icpResult2.op(); + serder = icpResult2.serder; + sigs = icpResult2.sigs; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(serder, sigers)); + atc = ims.substring(serder.size); + embeds = { + icp: [serder, atc], + }; + + smids = exn.a.smids; + recp = [aid1['state'], aid3['state']].map((state) => state['i']); + + await client2 + .exchanges() + .send( + 'member2', + 'multisig', + aid2, + '/multisig/icp', + { gid: serder.pre, smids: smids, rmids: smids }, + embeds, + recp + ); + console.log('Member2 joined multisig, waiting for others...'); + + // Third member check notifications and join the multisig + msgSaid = await waitAndMarkNotification(client3, '/multisig/icp'); + console.log('Member3 received exchange message to join multisig'); + + res = await client3.groups().getRequest(msgSaid); + exn = res[0].exn; + icp = exn.e.icp; + let icpResult3 = await client3.identifiers().create('multisig', { + algo: signify.Algos.group, + mhab: aid3, + isith: icp.kt, + nsith: icp.nt, + toad: parseInt(icp.bt), + wits: icp.b, + states: states, + rstates: rstates, + }); + op3 = await icpResult3.op(); + serder = icpResult3.serder; + sigs = icpResult3.sigs; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(serder, sigers)); + atc = ims.substring(serder.size); + embeds = { + icp: [serder, atc], + }; + + smids = exn.a.smids; + recp = [aid1['state'], aid2['state']].map((state) => state['i']); + + await client3 + .exchanges() + .send( + 'member3', + 'multisig', + aid3, + '/multisig/icp', + { gid: serder.pre, smids: smids, rmids: smids }, + embeds, + recp + ); + console.log('Member3 joined, multisig waiting for others...'); + + // Check for completion + op1 = await waitOperation(client1, op1); + op2 = await waitOperation(client2, op2); + op3 = await waitOperation(client3, op3); + console.log('Multisig created!'); + const identifiers1 = await client1.identifiers().list(); + assert.equal(identifiers1.aids.length, 2); + assert.equal(identifiers1.aids[0].name, 'member1'); + assert.equal(identifiers1.aids[1].name, 'multisig'); + + const identifiers2 = await client2.identifiers().list(); + assert.equal(identifiers2.aids.length, 2); + assert.equal(identifiers2.aids[0].name, 'member2'); + assert.equal(identifiers2.aids[1].name, 'multisig'); + + const identifiers3 = await client3.identifiers().list(); + assert.equal(identifiers3.aids.length, 2); + assert.equal(identifiers3.aids[0].name, 'member3'); + assert.equal(identifiers3.aids[1].name, 'multisig'); + + console.log( + 'Client 1 managed AIDs:\n', + identifiers1.aids[0].name, + `[${identifiers1.aids[0].prefix}]\n`, + identifiers1.aids[1].name, + `[${identifiers1.aids[1].prefix}]` + ); + console.log( + 'Client 2 managed AIDs:\n', + identifiers2.aids[0].name, + `[${identifiers2.aids[0].prefix}]\n`, + identifiers2.aids[1].name, + `[${identifiers2.aids[1].prefix}]` + ); + console.log( + 'Client 3 managed AIDs:\n', + identifiers3.aids[0].name, + `[${identifiers3.aids[0].prefix}]\n`, + identifiers3.aids[1].name, + `[${identifiers3.aids[1].prefix}]` + ); + + const multisig = identifiers3.aids[1].prefix; + + // Multisig end role + // for brevity, this script authorize only the agent of member 1 + // a full implementation should repeat the process to authorize all agents + + const members = await client1.identifiers().members('multisig'); + let hab = await client1.identifiers().get('multisig'); + let aid = hab['prefix']; + const signing = members['signing']; + const agentEnds1 = signing[0].ends.agent; + if (!agentEnds1) { + throw new Error('signing[0].ends.agent is null or undefined'); + } + const eid1 = Object.keys(agentEnds1)[0]; // agent of member 1 + // other agent eids can be obtained with + // let eid2 = Object.keys(signing[1].ends.agent)[0]; + // let eid3 = Object.keys(signing[2].ends.agent)[0]; + console.log(`Starting multisig end role authorization for agent ${eid1}`); + + // initial stamp for the event that will be passed in the exn message + // to the other members + let stamp = new Date().toISOString().replace('Z', '000+00:00'); + + let endRoleRes = await client1 + .identifiers() + .addEndRole('multisig', 'agent', eid1, stamp); + op1 = await endRoleRes.op(); + let rpy = endRoleRes.serder; + sigs = endRoleRes.sigs; + let mstate = hab['state']; + let seal = [ + 'SealEvent', + { i: hab['prefix'], s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + let roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + atc = roleims.substring(rpy.size); + let roleembeds = { + rpy: [rpy, atc], + }; + recp = [aid2['state'], aid3['state']].map((state) => state['i']); + res = await client1 + .exchanges() + .send( + 'member1', + 'multisig', + aid1, + '/multisig/rpy', + { gid: aid }, + roleembeds, + recp + ); + console.log( + `Member1 authorized agent role to ${eid1}, waiting for others to authorize...` + ); + + //Member2 check for notifications and join the authorization + msgSaid = await waitAndMarkNotification(client2, '/multisig/rpy'); + console.log( + 'Member2 received exchange message to join the end role authorization' + ); + res = await client2.groups().getRequest(msgSaid); + exn = res[0].exn; + // stamp, eid and role are provided in the exn message + let rpystamp = exn.e.rpy.dt; + let rpyrole = exn.e.rpy.a.role; + let rpyeid = exn.e.rpy.a.eid; + endRoleRes = await client2 + .identifiers() + .addEndRole('multisig', rpyrole, rpyeid, rpystamp); + op2 = await endRoleRes.op(); + rpy = endRoleRes.serder; + sigs = endRoleRes.sigs; + + hab = await client2.identifiers().get('multisig'); + mstate = hab['state']; + seal = [ + 'SealEvent', + { i: hab['prefix'], s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + atc = roleims.substring(rpy.size); + roleembeds = { + rpy: [rpy, atc], + }; + recp = [aid1['state'], aid3['state']].map((state) => state['i']); + res = await client2 + .exchanges() + .send( + 'member2', + 'multisig', + aid2, + '/multisig/rpy', + { gid: aid }, + roleembeds, + recp + ); + console.log( + `Member2 authorized agent role to ${eid1}, waiting for others to authorize...` + ); + + //Member3 check for notifications and join the authorization + msgSaid = await waitAndMarkNotification(client3, '/multisig/rpy'); + console.log( + 'Member3 received exchange message to join the end role authorization' + ); + res = await client3.groups().getRequest(msgSaid); + exn = res[0].exn; + rpystamp = exn.e.rpy.dt; + rpyrole = exn.e.rpy.a.role; + rpyeid = exn.e.rpy.a.eid; + endRoleRes = await client3 + .identifiers() + .addEndRole('multisig', rpyrole, rpyeid, rpystamp); + + op3 = await endRoleRes.op(); + rpy = endRoleRes.serder; + sigs = endRoleRes.sigs; + hab = await client3.identifiers().get('multisig'); + mstate = hab['state']; + seal = [ + 'SealEvent', + { i: hab['prefix'], s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + atc = roleims.substring(rpy.size); + roleembeds = { + rpy: [rpy, atc], + }; + recp = [aid1['state'], aid2['state']].map((state) => state['i']); + res = await client3 + .exchanges() + .send( + 'member3', + 'multisig', + aid3, + '/multisig/rpy', + { gid: aid }, + roleembeds, + recp + ); + console.log( + `Member3 authorized agent role to ${eid1}, waiting for others to authorize...` + ); + + // Check for completion + op1 = await waitOperation(client1, op1); + op2 = await waitOperation(client2, op2); + op3 = await waitOperation(client3, op3); + console.log(`End role authorization for agent ${eid1}completed!`); + + // Holder resolve multisig OOBI + const oobimultisig = await client1.oobis().get('multisig', 'agent'); + op4 = await client4.oobis().resolve(oobimultisig.oobis[0], 'multisig'); + op4 = await waitOperation(client4, op4); + console.log(`Holder resolved multisig OOBI`); + + // MultiSig Interaction + + // Member1 initiates an interaction event + let data = { + i: 'EBgew7O4yp8SBle0FU-wwN3GtnaroI0BQfBGAj33QiIG', + s: '0', + d: 'EBgew7O4yp8SBle0FU-wwN3GtnaroI0BQfBGAj33QiIG', + }; + let eventResponse1 = await client1.identifiers().interact('multisig', data); + op1 = await eventResponse1.op(); + serder = eventResponse1.serder; + sigs = eventResponse1.sigs; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(serder, sigers)); + atc = ims.substring(serder.size); + let xembeds = { + ixn: [serder, atc], + }; + + smids = states.map((state) => state['i']); + recp = [aid2['state'], aid3['state']].map((state) => state['i']); + + await client1 + .exchanges() + .send( + 'member1', + 'multisig', + aid1, + '/multisig/ixn', + { gid: serder.pre, smids: smids, rmids: smids }, + xembeds, + recp + ); + console.log( + 'Member1 initiates interaction event, waiting for others to join...' + ); + + // Member2 check for notifications and join the interaction event + msgSaid = await waitAndMarkNotification(client2, '/multisig/ixn'); + console.log( + 'Member2 received exchange message to join the interaction event' + ); + res = await client2.groups().getRequest(msgSaid); + exn = res[0].exn; + let ixn = exn.e.ixn; + data = ixn.a; + + icpResult2 = await client2.identifiers().interact('multisig', data); + op2 = await icpResult2.op(); + serder = icpResult2.serder; + sigs = icpResult2.sigs; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(serder, sigers)); + atc = ims.substring(serder.size); + xembeds = { + ixn: [serder, atc], + }; + + smids = exn.a.smids; + recp = [aid1['state'], aid3['state']].map((state) => state['i']); + + await client2 + .exchanges() + .send( + 'member2', + 'multisig', + aid2, + '/multisig/ixn', + { gid: serder.pre, smids: smids, rmids: smids }, + xembeds, + recp + ); + console.log('Member2 joins interaction event, waiting for others...'); + + // Member3 check for notifications and join the interaction event + msgSaid = await waitAndMarkNotification(client3, '/multisig/ixn'); + console.log( + 'Member3 received exchange message to join the interaction event' + ); + res = await client3.groups().getRequest(msgSaid); + exn = res[0].exn; + ixn = exn.e.ixn; + data = ixn.a; + + icpResult3 = await client3.identifiers().interact('multisig', data); + op3 = await icpResult3.op(); + serder = icpResult3.serder; + sigs = icpResult3.sigs; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(serder, sigers)); + atc = ims.substring(serder.size); + xembeds = { + ixn: [serder, atc], + }; + + smids = exn.a.smids; + recp = [aid1['state'], aid2['state']].map((state) => state['i']); + + await client3 + .exchanges() + .send( + 'member3', + 'multisig', + aid3, + '/multisig/ixn', + { gid: serder.pre, smids: smids, rmids: smids }, + xembeds, + recp + ); + console.log('Member3 joins interaction event, waiting for others...'); + + // Check for completion + op1 = await waitOperation(client1, op1); + op2 = await waitOperation(client2, op2); + op3 = await waitOperation(client3, op3); + console.log('Multisig interaction completed!'); + + // Members agree out of band to rotate keys + console.log('Members agree out of band to rotate keys'); + icpResult1 = await client1.identifiers().rotate('member1'); + op1 = await icpResult1.op(); + op1 = await waitOperation(client1, op1); + aid1 = await client1.identifiers().get('member1'); + + console.log('Member1 rotated keys'); + icpResult2 = await client2.identifiers().rotate('member2'); + op2 = await icpResult2.op(); + op2 = await waitOperation(client2, op2); + aid2 = await client2.identifiers().get('member2'); + console.log('Member2 rotated keys'); + icpResult3 = await client3.identifiers().rotate('member3'); + op3 = await icpResult3.op(); + op3 = await waitOperation(client3, op3); + aid3 = await client3.identifiers().get('member3'); + console.log('Member3 rotated keys'); + + // Update new key states + op1 = await client1.keyStates().query(aid2.prefix, '1'); + op1 = await waitOperation(client1, op1); + const aid2State = op1['response']; + op1 = await client1.keyStates().query(aid3.prefix, '1'); + op1 = await waitOperation(client1, op1); + const aid3State = op1['response']; + + op2 = await client2.keyStates().query(aid3.prefix, '1'); + op2 = await waitOperation(client2, op2); + op2 = await client2.keyStates().query(aid1.prefix, '1'); + op2 = await waitOperation(client2, op2); + const aid1State = op2['response']; + + op3 = await client3.keyStates().query(aid1.prefix, '1'); + op3 = await waitOperation(client3, op3); + op3 = await client3.keyStates().query(aid2.prefix, '1'); + op3 = await waitOperation(client3, op3); + + op4 = await client4.keyStates().query(aid1.prefix, '1'); + op4 = await waitOperation(client4, op4); + op4 = await client4.keyStates().query(aid2.prefix, '1'); + op4 = await waitOperation(client4, op4); + op4 = await client4.keyStates().query(aid3.prefix, '1'); + op4 = await waitOperation(client4, op4); + + rstates = [aid1State, aid2State, aid3State]; + states = rstates; + + // Multisig Rotation + + // Member1 initiates a rotation event + eventResponse1 = await client1 + .identifiers() + .rotate('multisig', { states: states, rstates: rstates }); + op1 = await eventResponse1.op(); + serder = eventResponse1.serder; + sigs = eventResponse1.sigs; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(serder, sigers)); + atc = ims.substring(serder.size); + let rembeds = { + rot: [serder, atc], + }; + + smids = states.map((state) => state['i']); + recp = [aid2State, aid3State].map((state) => state['i']); + + await client1 + .exchanges() + .send( + 'member1', + 'multisig', + aid1, + '/multisig/rot', + { gid: serder.pre, smids: smids, rmids: smids }, + rembeds, + recp + ); + console.log( + 'Member1 initiates rotation event, waiting for others to join...' + ); + + // Member2 check for notifications and join the rotation event + msgSaid = await waitAndMarkNotification(client2, '/multisig/rot'); + console.log('Member2 received exchange message to join the rotation event'); + + await new Promise((resolve) => setTimeout(resolve, 5000)); + res = await client2.groups().getRequest(msgSaid); + exn = res[0].exn; + + icpResult2 = await client2 + .identifiers() + .rotate('multisig', { states: states, rstates: rstates }); + op2 = await icpResult2.op(); + serder = icpResult2.serder; + sigs = icpResult2.sigs; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(serder, sigers)); + atc = ims.substring(serder.size); + rembeds = { + rot: [serder, atc], + }; + + smids = exn.a.smids; + recp = [aid1State, aid3State].map((state) => state['i']); + + await client2 + .exchanges() + .send( + 'member2', + 'multisig', + aid2, + '/multisig/ixn', + { gid: serder.pre, smids: smids, rmids: smids }, + rembeds, + recp + ); + console.log('Member2 joins rotation event, waiting for others...'); + + // Member3 check for notifications and join the rotation event + msgSaid = await waitAndMarkNotification(client3, '/multisig/rot'); + console.log('Member3 received exchange message to join the rotation event'); + res = await client3.groups().getRequest(msgSaid); + exn = res[0].exn; + + icpResult3 = await client3 + .identifiers() + .rotate('multisig', { states: states, rstates: rstates }); + op3 = await icpResult3.op(); + serder = icpResult3.serder; + sigs = icpResult3.sigs; + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(serder, sigers)); + atc = ims.substring(serder.size); + rembeds = { + rot: [serder, atc], + }; + + smids = exn.a.smids; + recp = [aid1State, aid2State].map((state) => state['i']); + + await client3 + .exchanges() + .send( + 'member3', + 'multisig', + aid3, + '/multisig/ixn', + { gid: serder.pre, smids: smids, rmids: smids }, + rembeds, + recp + ); + console.log('Member3 joins rotation event, waiting for others...'); + + // Check for completion + op1 = await waitOperation(client1, op1); + op2 = await waitOperation(client2, op2); + op3 = await waitOperation(client3, op3); + console.log('Multisig rotation completed!'); + + hab = await client1.identifiers().get('multisig'); + aid = hab['prefix']; + + // Multisig Registry creation + aid1 = await client1.identifiers().get('member1'); + aid2 = await client2.identifiers().get('member2'); + aid3 = await client3.identifiers().get('member3'); + + console.log('Starting multisig registry creation'); + + const vcpRes1 = await client1.registries().create({ + name: 'multisig', + registryName: 'vLEI Registry', + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + }); + op1 = await vcpRes1.op(); + serder = vcpRes1.regser; + const regk = serder.pre; + let anc = vcpRes1.serder; + sigs = vcpRes1.sigs; + + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(anc, sigers)); + atc = ims.substring(anc.size); + let regbeds = { + vcp: [serder, ''], + anc: [anc, atc], + }; + + recp = [aid2['state'], aid3['state']].map((state) => state['i']); + res = await client1 + .exchanges() + .send( + 'member1', + 'registry', + aid1, + '/multisig/vcp', + { gid: multisig, usage: 'Issue vLEIs' }, + regbeds, + recp + ); + + console.log('Member1 initiated registry, waiting for others to join...'); + + // Member2 check for notifications and join the create registry event + msgSaid = await waitAndMarkNotification(client2, '/multisig/vcp'); + console.log( + 'Member2 received exchange message to join the create registry event' + ); + res = await client2.groups().getRequest(msgSaid); + exn = res[0].exn; + + const vcpRes2 = await client2.registries().create({ + name: 'multisig', + registryName: 'vLEI Registry', + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + }); + op2 = await vcpRes2.op(); + serder = vcpRes2.regser; + anc = vcpRes2.serder; + sigs = vcpRes2.sigs; + + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(anc, sigers)); + atc = ims.substring(anc.size); + regbeds = { + vcp: [serder, ''], + anc: [anc, atc], + }; + + recp = [aid1['state'], aid3['state']].map((state) => state['i']); + await client2 + .exchanges() + .send( + 'member2', + 'registry', + aid2, + '/multisig/vcp', + { gid: multisig, usage: 'Issue vLEIs' }, + regbeds, + recp + ); + console.log('Member2 joins registry event, waiting for others...'); + + // Member3 check for notifications and join the create registry event + msgSaid = await waitAndMarkNotification(client3, '/multisig/vcp'); + console.log( + 'Member3 received exchange message to join the create registry event' + ); + + res = await client3.groups().getRequest(msgSaid); + exn = res[0].exn; + + const vcpRes3 = await client3.registries().create({ + name: 'multisig', + registryName: 'vLEI Registry', + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + }); + op3 = await vcpRes3.op(); + serder = vcpRes3.regser; + anc = vcpRes3.serder; + sigs = vcpRes3.sigs; + + sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + + ims = signify.d(signify.messagize(anc, sigers)); + atc = ims.substring(anc.size); + regbeds = { + vcp: [serder, ''], + anc: [anc, atc], + }; + + recp = [aid1['state'], aid2['state']].map((state) => state['i']); + await client3 + .exchanges() + .send( + 'member3', + 'multisig', + aid3, + '/multisig/vcp', + { gid: multisig, usage: 'Issue vLEIs' }, + regbeds, + recp + ); + + // Done + op1 = await waitOperation(client1, op1); + op2 = await waitOperation(client2, op2); + op3 = await waitOperation(client3, op3); + console.log('Multisig create registry completed!'); + + //Create Credential + console.log('Starting multisig credential creation'); + + const vcdata = { + LEI: '5493001KJTIIGC8Y1R17', + }; + const holder = aid4.prefix; + + const TIME = new Date().toISOString().replace('Z', '000+00:00'); + const credRes = await client1.credentials().issue('multisig', { + ri: regk, + s: SCHEMA_SAID, + a: { + i: holder, + dt: TIME, + ...vcdata, + }, + }); + op1 = credRes.op; + await multisigIssue(client1, 'member1', 'multisig', credRes); + + console.log( + 'Member1 initiated credential creation, waiting for others to join...' + ); + + // Member2 check for notifications and join the credential create event + msgSaid = await waitAndMarkNotification(client2, '/multisig/iss'); + console.log( + 'Member2 received exchange message to join the credential create event' + ); + res = await client2.groups().getRequest(msgSaid); + exn = res[0].exn; + + const credentialSaid = exn.e.acdc.d; + const credRes2 = await client2.credentials().issue('multisig', exn.e.acdc); + + op2 = credRes2.op; + await multisigIssue(client2, 'member2', 'multisig', credRes2); + console.log('Member2 joins credential create event, waiting for others...'); + + // Member3 check for notifications and join the create registry event + msgSaid = await waitAndMarkNotification(client3, '/multisig/iss'); + console.log( + 'Member3 received exchange message to join the credential create event' + ); + res = await client3.groups().getRequest(msgSaid); + exn = res[0].exn; + + const credRes3 = await client3.credentials().issue('multisig', exn.e.acdc); + + op3 = credRes3.op; + await multisigIssue(client3, 'member3', 'multisig', credRes3); + console.log('Member3 joins credential create event, waiting for others...'); + + // Check completion + op1 = await waitOperation(client1, op1); + op2 = await waitOperation(client2, op2); + op3 = await waitOperation(client3, op3); + console.log('Multisig create credential completed!'); + + const m = await client1.identifiers().get('multisig'); + + // Update states + op1 = await client1.keyStates().query(m.prefix, '4'); + op1 = await waitOperation(client1, op1); + op2 = await client2.keyStates().query(m.prefix, '4'); + op2 = await waitOperation(client2, op2); + op3 = await client3.keyStates().query(m.prefix, '4'); + op3 = await waitOperation(client3, op3); + op4 = await client4.keyStates().query(m.prefix, '4'); + op4 = await waitOperation(client4, op4); + + // IPEX grant message + console.log('Starting grant message'); + stamp = new Date().toISOString().replace('Z', '000+00:00'); + + const [grant, gsigs, end] = await client1.ipex().grant({ + senderName: 'multisig', + acdc: credRes.acdc, + anc: credRes.anc, + iss: credRes.iss, + recipient: holder, + datetime: stamp, + }); + + op1 = await client1 + .ipex() + .submitGrant('multisig', grant, gsigs, end, [holder]); + + mstate = m['state']; + seal = [ + 'SealEvent', + { i: m['prefix'], s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + sigers = gsigs.map((sig) => new signify.Siger({ qb64: sig })); + + let gims = signify.d(signify.messagize(grant, sigers, seal)); + atc = gims.substring(grant.size); + atc += end; + let gembeds = { + exn: [grant, atc], + }; + recp = [aid2['state'], aid3['state']].map((state) => state['i']); + await client1 + .exchanges() + .send( + 'member1', + 'multisig', + aid1, + '/multisig/exn', + { gid: m['prefix'] }, + gembeds, + recp + ); + + console.log( + 'Member1 initiated grant message, waiting for others to join...' + ); + + msgSaid = await waitAndMarkNotification(client2, '/multisig/exn'); + console.log('Member2 received exchange message to join the grant message'); + res = await client2.groups().getRequest(msgSaid); + exn = res[0].exn; + + const [grant2, gsigs2, end2] = await client2.ipex().grant({ + senderName: 'multisig', + recipient: holder, + acdc: credRes2.acdc, + anc: credRes2.anc, + iss: credRes3.iss, + datetime: stamp, + }); + + op2 = await client2 + .ipex() + .submitGrant('multisig', grant2, gsigs2, end2, [holder]); + + sigers = gsigs2.map((sig) => new signify.Siger({ qb64: sig })); + + gims = signify.d(signify.messagize(grant2, sigers, seal)); + atc = gims.substring(grant2.size); + atc += end2; + + gembeds = { + exn: [grant2, atc], + }; + recp = [aid1['state'], aid3['state']].map((state) => state['i']); + await client2 + .exchanges() + .send( + 'member2', + 'multisig', + aid2, + '/multisig/exn', + { gid: m['prefix'] }, + gembeds, + recp + ); + + console.log('Member2 joined grant message, waiting for others to join...'); + + msgSaid = await waitAndMarkNotification(client3, '/multisig/exn'); + console.log('Member3 received exchange message to join the grant message'); + res = await client3.groups().getRequest(msgSaid); + exn = res[0].exn; + + const [grant3, gsigs3, end3] = await client3.ipex().grant({ + senderName: 'multisig', + recipient: holder, + acdc: credRes3.acdc, + anc: credRes3.anc, + iss: credRes3.iss, + datetime: stamp, + }); + + op3 = await client3 + .ipex() + .submitGrant('multisig', grant3, gsigs3, end3, [holder]); + + sigers = gsigs3.map((sig) => new signify.Siger({ qb64: sig })); + + gims = signify.d(signify.messagize(grant3, sigers, seal)); + atc = gims.substring(grant3.size); + atc += end3; + + gembeds = { + exn: [grant3, atc], + }; + recp = [aid1['state'], aid2['state']].map((state) => state['i']); + await client3 + .exchanges() + .send( + 'member3', + 'multisig', + aid3, + '/multisig/exn', + { gid: m['prefix'] }, + gembeds, + recp + ); + + console.log('Member3 joined grant message, waiting for others to join...'); + + await Promise.all([ + waitOperation(client1, op1), + waitOperation(client2, op2), + waitOperation(client3, op3), + ]); + + msgSaid = await waitAndMarkNotification(client4, '/exn/ipex/grant', { + timeout: 30000, + }); + console.log('Holder received exchange message with the grant message'); + res = await client4.exchanges().get(msgSaid); + + const [admit, asigs, aend] = await client4.ipex().admit({ + senderName: 'holder', + message: '', + grantSaid: res.exn.d, + recipient: m['prefix'], + }); + + op4 = await client4 + .ipex() + .submitAdmit('holder', admit, asigs, aend, [m['prefix']]); + + await waitOperation(client4, op4); + + console.log('Holder creates and sends admit message'); + + msgSaid = await waitAndMarkNotification(client1, '/exn/ipex/admit', { + timeout: 30000, + }); + console.log('Member1 received exchange message with the admit response'); + const creds = await client4.credentials().list(); + console.log(`Holder holds ${creds.length} credential`); + + await assertOperations(client1, client2, client3, client4); + await warnNotifications(client1, client2, client3, client4); + + console.log('Revoking credential...'); + const REVTIME = new Date().toISOString().replace('Z', '000+00:00'); + const revokeRes = await client1 + .credentials() + .revoke('multisig', credentialSaid, REVTIME); + op1 = revokeRes.op; + + await multisigRevoke( + client1, + 'member1', + 'multisig', + revokeRes.rev, + revokeRes.anc + ); + + console.log( + 'Member1 initiated credential revocation, waiting for others to join...' + ); + + // Member2 check for notifications and join the credential create event + msgSaid = await waitAndMarkNotification(client2, '/multisig/rev'); + console.log( + 'Member2 received exchange message to join the credential revocation event' + ); + res = await client2.groups().getRequest(msgSaid); + + const revokeRes2 = await client2 + .credentials() + .revoke('multisig', credentialSaid, REVTIME); + + op2 = revokeRes2.op; + await multisigRevoke( + client2, + 'member2', + 'multisig', + revokeRes2.rev, + revokeRes2.anc + ); + console.log('Member2 joins credential revoke event, waiting for others...'); + + // Member3 check for notifications and join the create registry event + msgSaid = await waitAndMarkNotification(client3, '/multisig/rev'); + console.log( + 'Member3 received exchange message to join the credential revocation event' + ); + res = await client3.groups().getRequest(msgSaid); + + const revokeRes3 = await client3 + .credentials() + .revoke('multisig', credentialSaid, REVTIME); + + op3 = revokeRes3.op; + + await multisigRevoke( + client3, + 'member3', + 'multisig', + revokeRes3.rev, + revokeRes3.anc + ); + console.log('Member3 joins credential revoke event, waiting for others...'); + + // Check completion + op1 = await waitOperation(client1, op1); + op2 = await waitOperation(client2, op2); + op3 = await waitOperation(client3, op3); + console.log('Multisig credential revocation completed!'); +}, 400000); + +async function createAID(client: SignifyClient, name: string, wits: string[]) { + await getOrCreateIdentifier(client, name, { + wits: wits, + toad: wits.length, + }); + return await client.identifiers().get(name); +} + +async function multisigIssue( + client: SignifyClient, + memberName: string, + groupName: string, + result: IssueCredentialResult +) { + const leaderHab = await client.identifiers().get(memberName); + const groupHab = await client.identifiers().get(groupName); + const members = await client.identifiers().members(groupName); + + const keeper = client.manager!.get(groupHab); + const sigs = await keeper.sign(signify.b(result.anc.raw)); + const sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(result.anc, sigers)); + const atc = ims.substring(result.anc.size); + + const embeds = { + acdc: [result.acdc, ''], + iss: [result.iss, ''], + anc: [result.anc, atc], + }; + + const recipients = members.signing + .map((m: { aid: string }) => m.aid) + .filter((aid: string) => aid !== leaderHab.prefix); + + await client + .exchanges() + .send( + memberName, + 'multisig', + leaderHab, + '/multisig/iss', + { gid: groupHab.prefix }, + embeds, + recipients + ); +} + +async function multisigRevoke( + client: SignifyClient, + memberName: string, + groupName: string, + rev: Serder, + anc: Serder +) { + const leaderHab = await client.identifiers().get(memberName); + const groupHab = await client.identifiers().get(groupName); + const members = await client.identifiers().members(groupName); + + const keeper = client.manager!.get(groupHab); + const sigs = await keeper.sign(signify.b(anc.raw)); + const sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(anc, sigers)); + const atc = ims.substring(anc.size); + + const embeds = { + iss: [rev, ''], + anc: [anc, atc], + }; + + const recipients = members.signing + .map((m: { aid: string }) => m.aid) + .filter((aid: string) => aid !== leaderHab.prefix); + + await client + .exchanges() + .send( + memberName, + 'multisig', + leaderHab, + '/multisig/rev', + { gid: groupHab.prefix }, + embeds, + recipients + ); +} diff --git a/packages/signify-ts/test-integration/randy.test.ts b/packages/signify-ts/test-integration/randy.test.ts new file mode 100644 index 00000000..5d900e71 --- /dev/null +++ b/packages/signify-ts/test-integration/randy.test.ts @@ -0,0 +1,63 @@ +import { assert, test } from 'vitest'; +import signify from 'signify-ts'; +import { + assertOperations, + getOrCreateClient, + waitOperation, +} from './utils/test-util.ts'; + +test('randy', async () => { + const client1 = await getOrCreateClient(); + + let icpResult = await client1 + .identifiers() + .create('aid1', { algo: signify.Algos.randy }); + let op = await waitOperation(client1, await icpResult.op()); + assert.equal(op['done'], true); + let aid = op['response']; + const icp = new signify.Serder(aid); + assert.equal(icp.verfers.length, 1); + assert.equal(icp.digers.length, 1); + assert.equal(icp.sad['kt'], '1'); + assert.equal(icp.sad['nt'], '1'); + + let aids = await client1.identifiers().list(); + assert.equal(aids.aids.length, 1); + aid = aids.aids[0]; + assert.equal(aid.name, 'aid1'); + assert.equal(aid.prefix, icp.pre); + + icpResult = await client1.identifiers().interact('aid1', [icp.pre]); + op = await waitOperation(client1, await icpResult.op()); + let ked = op['response']; + const ixn = new signify.Serder(ked); + assert.equal(ixn.sad['s'], '1'); + assert.deepEqual([...ixn.sad['a']], [icp.pre]); + + aids = await client1.identifiers().list(); + assert.equal(aids.aids.length, 1); + aid = aids.aids[0]; + + const events = client1.keyEvents(); + let log = await events.get(aid['prefix']); + assert.equal(log.length, 2); + + icpResult = await client1.identifiers().rotate('aid1'); + op = await waitOperation(client1, await icpResult.op()); + ked = op['response']; + const rot = new signify.Serder(ked); + assert.equal(rot.sad['s'], '2'); + assert.equal(rot.verfers.length, 1); + assert.equal(rot.digers.length, 1); + assert.notEqual(rot.verfers[0].qb64, icp.verfers[0].qb64); + assert.notEqual(rot.digers[0].qb64, icp.digers[0].qb64); + const dig = new signify.Diger( + { code: signify.MtrDex.Blake3_256 }, + rot.verfers[0].qb64b + ); + assert.equal(dig.qb64, icp.digers[0].qb64); + log = await events.get(aid['prefix']); + assert.equal(log.length, 3); + + await assertOperations(client1); +}, 30000); diff --git a/packages/signify-ts/test-integration/salty.test.ts b/packages/signify-ts/test-integration/salty.test.ts new file mode 100644 index 00000000..a560b9aa --- /dev/null +++ b/packages/signify-ts/test-integration/salty.test.ts @@ -0,0 +1,165 @@ +import { assert, test } from 'vitest'; +import signify from 'signify-ts'; +import { + assertOperations, + getOrCreateClient, + waitOperation, +} from './utils/test-util.ts'; + +test('salty', async () => { + const client1 = await getOrCreateClient(); + + let icpResult = await client1 + .identifiers() + .create('aid1', { bran: '0123456789abcdefghijk' }); + let op = await waitOperation(client1, await icpResult.op()); + + const aid1 = op['response']; + const icp = new signify.Serder(aid1); + assert.equal(icp.pre, 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK'); + assert.equal(icp.verfers.length, 1); + assert.equal( + icp.verfers[0].qb64, + 'DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9' + ); + assert.equal(icp.digers.length, 1); + assert.equal( + icp.digers[0].qb64, + 'EAORnRtObOgNiOlMolji-KijC_isa3lRDpHCsol79cOc' + ); + assert.equal(icp.sad['kt'], '1'); + assert.equal(icp.sad['nt'], '1'); + let aids = await client1.identifiers().list(); + assert.equal(aids.aids.length, 1); + let aid = aids.aids.pop(); + assert.equal(aid.name, 'aid1'); + let salt = aid.salty; + assert.equal(salt.pidx, 0); + assert.equal(salt.stem, 'signify:aid'); + assert.equal(aid.prefix, icp.pre); + + icpResult = await client1.identifiers().create('aid2', { + count: 3, + ncount: 3, + isith: '2', + nsith: '2', + bran: '0123456789lmnopqrstuv', + }); + op = await waitOperation(client1, await icpResult.op()); + const aid2 = op['response']; + const icp2 = new signify.Serder(aid2); + assert.equal(icp2.pre, 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'); + assert.equal(icp2.verfers.length, 3); + assert.equal( + icp2.verfers[0].qb64, + 'DGBw7C7AfC7jbD3jLLRS3SzIWFndM947TyNWKQ52iQx5' + ); + assert.equal( + icp2.verfers[1].qb64, + 'DD_bHYFsgWXuCbz3SD0HjCIe_ITjRvEoCGuZ4PcNFFDz' + ); + assert.equal( + icp2.verfers[2].qb64, + 'DEe9u8k0fm1wMFAuOIsCtCNrpduoaV5R21rAcJl0awze' + ); + assert.equal(icp2.digers.length, 3); + assert.equal( + icp2.digers[0].qb64, + 'EML5FrjCpz8SEl4dh0U15l8bMRhV_O5iDcR1opLJGBSH' + ); + assert.equal( + icp2.digers[1].qb64, + 'EJpKquuibYTqpwMDqEFAFs0gwq0PASAHZ_iDmSF3I2Vg' + ); + assert.equal( + icp2.digers[2].qb64, + 'ELplTAiEKdobFhlf-dh1vUb2iVDW0dYOSzs1dR7fQo60' + ); + assert.equal(icp2.sad['kt'], '2'); + assert.equal(icp2.sad['nt'], '2'); + aids = await client1.identifiers().list(); + assert.equal(aids.aids.length, 2); + aid = aids.aids[1]; + assert.equal(aid.name, 'aid2'); + salt = aid.salty; + assert.equal(salt.pidx, 1); + assert.equal(salt.stem, 'signify:aid'); + assert.equal(aid.prefix, icp2.pre); + + icpResult = await client1.identifiers().create('aid3'); + await waitOperation(client1, await icpResult.op()); + aids = await client1.identifiers().list(); + assert.equal(aids.aids.length, 3); + aid = aids.aids[0]; + assert.equal(aid.name, 'aid1'); + + aids = await client1.identifiers().list(1, 2); + assert.equal(aids.aids.length, 2); + aid = aids.aids[0]; + assert.equal(aid.name, 'aid2'); + + aids = await client1.identifiers().list(2, 2); + assert.equal(aids.aids.length, 1); + aid = aids.aids[0]; + assert.equal(aid.name, 'aid3'); + + icpResult = await client1.identifiers().rotate('aid1'); + op = await waitOperation(client1, await icpResult.op()); + let ked = op['response']; + const rot = new signify.Serder(ked); + assert.equal(rot.sad['d'], 'EBQABdRgaxJONrSLcgrdtbASflkvLxJkiDO0H-XmuhGg'); + assert.equal(rot.sad['s'], '1'); + assert.equal(rot.verfers.length, 1); + assert.equal(rot.digers.length, 1); + assert.equal( + rot.verfers[0].qb64, + 'DHgomzINlGJHr-XP3sv2ZcR9QsIEYS3LJhs4KRaZYKly' + ); + assert.equal( + rot.digers[0].qb64, + 'EJMovBlrBuD6BVeUsGSxLjczbLEbZU9YnTSud9K4nVzk' + ); + + icpResult = await client1.identifiers().interact('aid1', [icp.pre]); + op = await waitOperation(client1, await icpResult.op()); + ked = op['response']; + const ixn = new signify.Serder(ked); + assert.equal(ixn.sad['d'], 'ENsmRAg_oM7Hl1S-GTRMA7s4y760lQMjzl0aqOQ2iTce'); + assert.equal(ixn.sad['s'], '2'); + assert.deepEqual([...ixn.sad['a']], [icp.pre]); + + aid = await client1.identifiers().get('aid1'); + const state = aid['state']; + + assert.equal(state['s'], '2'); + assert.equal(state['f'], '2'); + assert.equal(state['et'], 'ixn'); + assert.equal(state['d'], ixn.sad['d']); + assert.equal(state['ee']['d'], rot.sad['d']); + + const events = client1.keyEvents(); + const log = await events.get(aid['prefix']); + assert.equal(log.length, 3); + let serder = new signify.Serder(log[0]['ked']); + assert.equal(serder.pre, icp.pre); + assert.equal(serder.sad['d'], icp.sad['d']); + serder = new signify.Serder(log[1]['ked']); + assert.equal(serder.pre, rot.pre); + assert.equal(serder.sad['d'], rot.sad['d']); + serder = new signify.Serder(log[2]['ked']); + assert.equal(serder.pre, ixn.pre); + assert.equal(serder.sad['d'], ixn.sad['d']); + + await assertOperations(client1); + + aid = await client1.identifiers().update('aid3', { name: 'aid4' }); + assert.equal(aid.name, 'aid4'); + aid = await client1.identifiers().get('aid4'); + assert.equal(aid.name, 'aid4'); + aids = await client1.identifiers().list(2, 2); + assert.equal(aids.aids.length, 1); + aid = aids.aids[0]; + assert.equal(aid.name, 'aid4'); + + console.log('Salty test passed'); +}, 30000); diff --git a/packages/signify-ts/test-integration/singlesig-dip.test.ts b/packages/signify-ts/test-integration/singlesig-dip.test.ts new file mode 100644 index 00000000..13e35d91 --- /dev/null +++ b/packages/signify-ts/test-integration/singlesig-dip.test.ts @@ -0,0 +1,100 @@ +import { afterAll, assert, beforeAll, describe, test } from 'vitest'; +import { CreateIdentiferArgs, SignifyClient } from 'signify-ts'; +import { + assertOperations, + getOrCreateClients, + getOrCreateContact, + getOrCreateIdentifier, + waitOperation, +} from './utils/test-util.ts'; +import { resolveEnvironment } from './utils/resolve-env.ts'; + +let client1: SignifyClient, client2: SignifyClient; +let name1_id: string, name1_oobi: string; +let contact1_id: string; + +beforeAll(async () => { + [client1, client2] = await getOrCreateClients(2); +}); +beforeAll(async () => { + [name1_id, name1_oobi] = await getOrCreateIdentifier(client1, 'name1'); +}); +beforeAll(async () => { + contact1_id = await getOrCreateContact(client2, 'contact1', name1_oobi); +}); +afterAll(async () => { + await assertOperations(client1, client2); +}); + +describe('singlesig-dip', () => { + test('delegate1a', async () => { + // delegate creates identifier without witnesses + let kargs: CreateIdentiferArgs = { + delpre: name1_id, + }; + let result = await client2.identifiers().create('delegate1', kargs); + let op = await result.op(); + let delegate1 = await client2.identifiers().get('delegate1'); + assert.equal(op.name, `delegation.${delegate1.prefix}`); + + delegate1 = await client2.identifiers().get('delegate1'); + let seal = { + i: delegate1.prefix, + s: '0', + d: delegate1.prefix, + }; + result = await client1.identifiers().interact('name1', seal); + let op1 = await result.op(); + + // refresh keystate to sn=1 + let op2 = await client2.keyStates().query(name1_id, '1'); + + await Promise.all([ + (op = await waitOperation(client2, op)), + waitOperation(client1, op1), + waitOperation(client2, op2), + ]); + + delegate1 = await client2.identifiers().get('delegate1'); + assert.equal(delegate1.prefix, op.response.i); + + // delegate creates identifier with default witness config + const env = resolveEnvironment(); + kargs = { + delpre: name1_id, + toad: env.witnessIds.length, + wits: env.witnessIds, + }; + result = await client2.identifiers().create('delegate2', kargs); + op = await result.op(); + let delegate2 = await client2.identifiers().get('delegate2'); + assert.equal(op.name, `delegation.${delegate2.prefix}`); + + // delegator approves delegate + delegate2 = await client2.identifiers().get('delegate2'); + seal = { + i: delegate2.prefix, + s: '0', + d: delegate2.prefix, + }; + result = await client1.identifiers().interact('name1', seal); + op1 = await result.op(); + + // refresh keystate to seal event + op2 = await client2.keyStates().query(name1_id, undefined, seal); + + await Promise.all([ + (op = await waitOperation(client2, op)), + waitOperation(client1, op1), + waitOperation(client2, op2), + ]); + + // delegate waits for completion + delegate2 = await client2.identifiers().get('delegate2'); + assert.equal(delegate2.prefix, op.response.i); + + // make sure query with seal is idempotent + op = await client2.keyStates().query(name1_id, undefined, seal); + await waitOperation(client2, op); + }); +}); diff --git a/packages/signify-ts/test-integration/singlesig-drt.test.ts b/packages/signify-ts/test-integration/singlesig-drt.test.ts new file mode 100644 index 00000000..d8e3cab1 --- /dev/null +++ b/packages/signify-ts/test-integration/singlesig-drt.test.ts @@ -0,0 +1,83 @@ +import { afterAll, assert, beforeAll, describe, test } from 'vitest'; +import { CreateIdentiferArgs, SignifyClient } from 'signify-ts'; +import { + assertOperations, + getOrCreateClients, + getOrCreateContact, + getOrCreateIdentifier, + waitOperation, +} from './utils/test-util.ts'; + +let delegator: SignifyClient, delegate: SignifyClient; +let name1_id: string, name1_oobi: string; +let contact1_id: string; + +beforeAll(async () => { + [delegator, delegate] = await getOrCreateClients(2); +}); +beforeAll(async () => { + [name1_id, name1_oobi] = await getOrCreateIdentifier(delegator, 'name1'); +}); +beforeAll(async () => { + contact1_id = await getOrCreateContact(delegate, 'contact1', name1_oobi); +}); +afterAll(async () => { + await assertOperations(delegator, delegate); +}); + +describe('singlesig-drt', () => { + test('delegate1a', async () => { + // delegate creates identifier without witnesses + let kargs: CreateIdentiferArgs = { + delpre: name1_id, + }; + let result = await delegate.identifiers().create('delegate1', kargs); + let op = await result.op(); + let delegate1 = await delegate.identifiers().get('delegate1'); + assert.equal(op.name, `delegation.${delegate1.prefix}`); + + // delegator approves delegate + let seal = { + i: delegate1.prefix, + s: '0', + d: delegate1.prefix, + }; + result = await delegator.identifiers().interact('name1', seal); + let op1 = await result.op(); + + let op2 = await delegate.keyStates().query(name1_id, '1'); + + await Promise.all([ + waitOperation(delegate, op), + waitOperation(delegator, op1), + waitOperation(delegate, op2), + ]); + + kargs = {}; + result = await delegate.identifiers().rotate('delegate1', kargs); + op = await result.op(); + assert.equal(op.name, `delegation.${result.serder.sad.d}`); + + // delegator approves delegate + delegate1 = await delegate.identifiers().get('delegate1'); + + seal = { + i: delegate1.prefix, + s: '1', + d: delegate1.state.d, + }; + + result = await delegator.identifiers().interact('name1', seal); + op1 = await result.op(); + op2 = await delegate.keyStates().query(name1_id, '2'); + + await Promise.all([ + (op = await waitOperation(delegate, op)), + waitOperation(delegator, op1), + waitOperation(delegate, op2), + ]); + + assert.equal(op.response.t, `drt`); + assert.equal(op.response.s, `1`); + }); +}); diff --git a/packages/signify-ts/test-integration/singlesig-ixn.test.ts b/packages/signify-ts/test-integration/singlesig-ixn.test.ts new file mode 100644 index 00000000..aa7513c1 --- /dev/null +++ b/packages/signify-ts/test-integration/singlesig-ixn.test.ts @@ -0,0 +1,84 @@ +import { EventResult, SignifyClient } from 'signify-ts'; +import { + assertOperations, + getOrCreateClients, + getOrCreateContact, + getOrCreateIdentifier, + waitOperation, +} from './utils/test-util.ts'; +import { afterAll, assert, beforeAll, describe, expect, test } from 'vitest'; + +let client1: SignifyClient, client2: SignifyClient; +let name1_id: string, name1_oobi: string; +let contact1_id: string; + +beforeAll(async () => { + [client1, client2] = await getOrCreateClients(2); +}); +beforeAll(async () => { + [name1_id, name1_oobi] = await getOrCreateIdentifier(client1, 'name1'); +}); +beforeAll(async () => { + contact1_id = await getOrCreateContact(client2, 'contact1', name1_oobi); +}); +afterAll(async () => { + await assertOperations(client1, client2); +}); + +interface KeyState { + i: string; + s: string; + [property: string]: any; +} + +describe('singlesig-ixn', () => { + test('step1', async () => { + assert.equal(name1_id, contact1_id); + + const keystate1 = await client1.keyStates().get(name1_id); + assert.strictEqual(keystate1.length, 1); + + const keystate2 = await client2.keyStates().get(contact1_id); + assert.strictEqual(keystate2.length, 1); + + // local and remote keystate sequence match + assert.equal(keystate1[0].s, keystate2[0].s); + }); + test('ixn1', async () => { + // local keystate before ixn + const keystate0: KeyState = ( + await client1.keyStates().get(name1_id) + ).at(0); + expect(keystate0).not.toBeNull(); + + // ixn + const result: EventResult = await client1 + .identifiers() + .interact('name1', {}); + await waitOperation(client1, await result.op()); + + // local keystate after ixn + const keystate1: KeyState = ( + await client1.keyStates().get(name1_id) + ).at(0); + expect(parseInt(keystate1.s)).toBeGreaterThan(0); + // sequence has incremented + assert.equal(parseInt(keystate1.s), parseInt(keystate0.s) + 1); + + // remote keystate after ixn + const keystate2: KeyState = ( + await client2.keyStates().get(contact1_id) + ).at(0); + // remote keystate is one behind + assert.equal(parseInt(keystate2.s), parseInt(keystate1.s) - 1); + + // refresh remote keystate + let op = await client2 + .keyStates() + .query(contact1_id, keystate1.s, undefined); + op = await waitOperation(client2, op); + const keystate3: KeyState = op.response; + // local and remote keystate match + assert.equal(keystate3.s, keystate1.s); + }); +}); diff --git a/packages/signify-ts/test-integration/singlesig-rot.test.ts b/packages/signify-ts/test-integration/singlesig-rot.test.ts new file mode 100644 index 00000000..cf07b070 --- /dev/null +++ b/packages/signify-ts/test-integration/singlesig-rot.test.ts @@ -0,0 +1,95 @@ +import { afterAll, assert, beforeAll, describe, expect, test } from 'vitest'; +import { EventResult, RotateIdentifierArgs, SignifyClient } from 'signify-ts'; +import { + assertOperations, + getOrCreateClients, + getOrCreateContact, + getOrCreateIdentifier, + waitOperation, +} from './utils/test-util.ts'; + +let client1: SignifyClient, client2: SignifyClient; +let name1_id: string, name1_oobi: string; +let contact1_id: string; + +beforeAll(async () => { + [client1, client2] = await getOrCreateClients(2); +}); +beforeAll(async () => { + [name1_id, name1_oobi] = await getOrCreateIdentifier(client1, 'name1'); +}); +beforeAll(async () => { + contact1_id = await getOrCreateContact(client2, 'contact1', name1_oobi); +}); +afterAll(async () => { + await assertOperations(client1, client2); +}); + +interface KeyState { + i: string; + s: string; + k: string[]; + n: string[]; + [property: string]: any; +} + +describe('singlesig-rot', () => { + test('step1', async () => { + assert.equal(name1_id, contact1_id); + + const keystate1 = await client1.keyStates().get(name1_id); + assert.strictEqual(keystate1.length, 1); + + const keystate2 = await client2.keyStates().get(contact1_id); + assert.strictEqual(keystate2.length, 1); + + // local and remote keystate sequence match + assert.equal(keystate1[0].s, keystate2[0].s); + }); + test('rot1', async () => { + // local keystate before rot + const keystate0: KeyState = ( + await client1.keyStates().get(name1_id) + ).at(0); + expect(keystate0).not.toBeNull(); + assert.strictEqual(keystate0.k.length, 1); + assert.strictEqual(keystate0.n.length, 1); + + // rot + const args: RotateIdentifierArgs = {}; + const result: EventResult = await client1 + .identifiers() + .rotate('name1', args); + await waitOperation(client1, await result.op()); + + // local keystate after rot + const keystate1: KeyState = ( + await client1.keyStates().get(name1_id) + ).at(0); + expect(parseInt(keystate1.s)).toBeGreaterThan(0); + // sequence has incremented + assert.equal(parseInt(keystate1.s), parseInt(keystate0.s) + 1); + // current keys changed + expect(keystate1.k[0]).not.toEqual(keystate0.k[0]); + // next key hashes changed + expect(keystate1.n[0]).not.toEqual(keystate0.n[0]); + + // remote keystate after rot + const keystate2: KeyState = ( + await client2.keyStates().get(contact1_id) + ).at(0); + // remote keystate is one behind + assert.equal(parseInt(keystate2.s), parseInt(keystate1.s) - 1); + + // refresh remote keystate + let op = await client2 + .keyStates() + .query(contact1_id, keystate1.s, undefined); + op = await waitOperation(client2, op); + const keystate3: KeyState = op.response; + // local and remote keystate match + assert.equal(keystate3.s, keystate1.s); + assert.equal(keystate3.k[0], keystate1.k[0]); + assert.equal(keystate3.n[0], keystate1.n[0]); + }); +}); diff --git a/packages/signify-ts/test-integration/singlesig-vlei-issuance.test.ts b/packages/signify-ts/test-integration/singlesig-vlei-issuance.test.ts new file mode 100644 index 00000000..f4b6e2dd --- /dev/null +++ b/packages/signify-ts/test-integration/singlesig-vlei-issuance.test.ts @@ -0,0 +1,548 @@ +import { assert, test } from 'vitest'; +import { Saider, Serder, SignifyClient } from 'signify-ts'; +import { resolveEnvironment } from './utils/resolve-env.ts'; +import { + Aid, + assertOperations, + createAid, + getOrCreateClients, + getOrCreateContact, + getOrIssueCredential, + getReceivedCredential, + markAndRemoveNotification, + resolveOobi, + waitForNotifications, + waitOperation, + warnNotifications, +} from './utils/test-util.ts'; +import { retry } from './utils/retry.ts'; + +const { vleiServerUrl } = resolveEnvironment(); + +const QVI_SCHEMA_SAID = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; +const LE_SCHEMA_SAID = 'ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY'; +const ECR_AUTH_SCHEMA_SAID = 'EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g'; +const ECR_SCHEMA_SAID = 'EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw'; +const OOR_AUTH_SCHEMA_SAID = 'EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E'; +const OOR_SCHEMA_SAID = 'EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy'; + +const vLEIServerHostUrl = `${vleiServerUrl}/oobi`; +const QVI_SCHEMA_URL = `${vLEIServerHostUrl}/${QVI_SCHEMA_SAID}`; +const LE_SCHEMA_URL = `${vLEIServerHostUrl}/${LE_SCHEMA_SAID}`; +const ECR_AUTH_SCHEMA_URL = `${vLEIServerHostUrl}/${ECR_AUTH_SCHEMA_SAID}`; +const ECR_SCHEMA_URL = `${vLEIServerHostUrl}/${ECR_SCHEMA_SAID}`; +const OOR_AUTH_SCHEMA_URL = `${vLEIServerHostUrl}/${OOR_AUTH_SCHEMA_SAID}`; +const OOR_SCHEMA_URL = `${vLEIServerHostUrl}/${OOR_SCHEMA_SAID}`; + +const qviData = { + LEI: '254900OPPU84GM83MG36', +}; + +const leData = { + LEI: '875500ELOZEL05BVXV37', +}; + +const ecrData = { + LEI: leData.LEI, + personLegalName: 'John Doe', + engagementContextRole: 'EBA Data Submitter', +}; + +const ecrAuthData = { + AID: '', + LEI: ecrData.LEI, + personLegalName: ecrData.personLegalName, + engagementContextRole: ecrData.engagementContextRole, +}; + +const oorData = { + LEI: leData.LEI, + personLegalName: 'John Doe', + officialRole: 'HR Manager', +}; + +const oorAuthData = { + AID: '', + LEI: oorData.LEI, + personLegalName: oorData.personLegalName, + officialRole: oorData.officialRole, +}; + +const LE_RULES = Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, +})[1]; + +const ECR_RULES = Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, + privacyDisclaimer: { + l: 'It is the sole responsibility of Holders as Issuees of an ECR vLEI Credential to present that Credential in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification.', + }, +})[1]; + +const ECR_AUTH_RULES = Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, + privacyDisclaimer: { + l: 'Privacy Considerations are applicable to QVI ECR AUTH vLEI Credentials. It is the sole responsibility of QVIs as Issuees of QVI ECR AUTH vLEI Credentials to present these Credentials in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification.', + }, +})[1]; + +const OOR_RULES = LE_RULES; +const OOR_AUTH_RULES = LE_RULES; + +const CRED_RETRY_DEFAULTS = { + maxSleep: 10000, + minSleep: 1000, + maxRetries: undefined, + timeout: 30000, +}; + +function createTimestamp() { + return new Date().toISOString().replace('Z', '000+00:00'); +} + +test('singlesig-vlei-issuance', async function run() { + const [gleifClient, qviClient, leClient, roleClient] = + await getOrCreateClients(4); + + const [gleifAid, qviAid, leAid, roleAid] = await Promise.all([ + createAid(gleifClient, 'gleif'), + createAid(qviClient, 'qvi'), + createAid(leClient, 'le'), + createAid(roleClient, 'role'), + ]); + + await Promise.all([ + getOrCreateContact(gleifClient, 'qvi', qviAid.oobi), + getOrCreateContact(qviClient, 'gleif', gleifAid.oobi), + getOrCreateContact(qviClient, 'le', leAid.oobi), + getOrCreateContact(qviClient, 'role', roleAid.oobi), + getOrCreateContact(leClient, 'gleif', gleifAid.oobi), + getOrCreateContact(leClient, 'qvi', qviAid.oobi), + getOrCreateContact(leClient, 'role', roleAid.oobi), + getOrCreateContact(roleClient, 'gleif', gleifAid.oobi), + getOrCreateContact(roleClient, 'qvi', qviAid.oobi), + getOrCreateContact(roleClient, 'le', leAid.oobi), + ]); + + await Promise.all([ + resolveOobi(gleifClient, QVI_SCHEMA_URL), + resolveOobi(qviClient, QVI_SCHEMA_URL), + resolveOobi(qviClient, LE_SCHEMA_URL), + resolveOobi(qviClient, ECR_AUTH_SCHEMA_URL), + resolveOobi(qviClient, ECR_SCHEMA_URL), + resolveOobi(qviClient, OOR_AUTH_SCHEMA_URL), + resolveOobi(qviClient, OOR_SCHEMA_URL), + resolveOobi(leClient, QVI_SCHEMA_URL), + resolveOobi(leClient, LE_SCHEMA_URL), + resolveOobi(leClient, ECR_AUTH_SCHEMA_URL), + resolveOobi(leClient, ECR_SCHEMA_URL), + resolveOobi(leClient, OOR_AUTH_SCHEMA_URL), + resolveOobi(leClient, OOR_SCHEMA_URL), + resolveOobi(roleClient, QVI_SCHEMA_URL), + resolveOobi(roleClient, LE_SCHEMA_URL), + resolveOobi(roleClient, ECR_AUTH_SCHEMA_URL), + resolveOobi(roleClient, ECR_SCHEMA_URL), + resolveOobi(roleClient, OOR_AUTH_SCHEMA_URL), + resolveOobi(roleClient, OOR_SCHEMA_URL), + ]); + + const [gleifRegistry, qviRegistry, leRegistry] = await Promise.all([ + getOrCreateRegistry(gleifClient, gleifAid, 'gleifRegistry'), + getOrCreateRegistry(qviClient, qviAid, 'qviRegistry'), + getOrCreateRegistry(leClient, leAid, 'leRegistry'), + ]); + + console.log('Issuing QVI vLEI Credential'); + const qviCred = await getOrIssueCredential( + gleifClient, + gleifAid, + qviAid, + gleifRegistry, + qviData, + QVI_SCHEMA_SAID + ); + + let qviCredHolder = await getReceivedCredential(qviClient, qviCred.sad.d); + + if (!qviCredHolder) { + await sendGrantMessage(gleifClient, gleifAid, qviAid, qviCred); + await sendAdmitMessage(qviClient, qviAid, gleifAid); + + qviCredHolder = await retry(async () => { + const cred = await getReceivedCredential(qviClient, qviCred.sad.d); + assert(cred !== undefined); + return cred; + }, CRED_RETRY_DEFAULTS); + } + + assert.equal(qviCredHolder.sad.d, qviCred.sad.d); + assert.equal(qviCredHolder.sad.s, QVI_SCHEMA_SAID); + assert.equal(qviCredHolder.sad.i, gleifAid.prefix); + assert.equal(qviCredHolder.sad.a.i, qviAid.prefix); + assert.equal(qviCredHolder.status.s, '0'); + assert(qviCredHolder.atc !== undefined); + + console.log('Issuing LE vLEI Credential'); + const leCredSource = Saider.saidify({ + d: '', + qvi: { + n: qviCred.sad.d, + s: qviCred.sad.s, + }, + })[1]; + + const leCred = await getOrIssueCredential( + qviClient, + qviAid, + leAid, + qviRegistry, + leData, + LE_SCHEMA_SAID, + LE_RULES, + leCredSource + ); + + let leCredHolder = await getReceivedCredential(leClient, leCred.sad.d); + + if (!leCredHolder) { + await sendGrantMessage(qviClient, qviAid, leAid, leCred); + await sendAdmitMessage(leClient, leAid, qviAid); + + leCredHolder = await retry(async () => { + const cred = await getReceivedCredential(leClient, leCred.sad.d); + assert(cred !== undefined); + return cred; + }, CRED_RETRY_DEFAULTS); + } + + assert.equal(leCredHolder.sad.d, leCred.sad.d); + assert.equal(leCredHolder.sad.s, LE_SCHEMA_SAID); + assert.equal(leCredHolder.sad.i, qviAid.prefix); + assert.equal(leCredHolder.sad.a.i, leAid.prefix); + assert.equal(leCredHolder.sad.e.qvi.n, qviCred.sad.d); + assert.equal(leCredHolder.status.s, '0'); + assert(leCredHolder.atc !== undefined); + + console.log('Issuing ECR vLEI Credential from LE'); + const ecrCredSource = Saider.saidify({ + d: '', + le: { + n: leCred.sad.d, + s: leCred.sad.s, + }, + })[1]; + + const ecrCred = await getOrIssueCredential( + leClient, + leAid, + roleAid, + leRegistry, + ecrData, + ECR_SCHEMA_SAID, + ECR_RULES, + ecrCredSource, + true + ); + + let ecrCredHolder = await getReceivedCredential(roleClient, ecrCred.sad.d); + + if (!ecrCredHolder) { + await sendGrantMessage(leClient, leAid, roleAid, ecrCred); + await sendAdmitMessage(roleClient, roleAid, leAid); + + ecrCredHolder = await retry(async () => { + const cred = await getReceivedCredential(roleClient, ecrCred.sad.d); + assert(cred !== undefined); + return cred; + }, CRED_RETRY_DEFAULTS); + } + + assert.equal(ecrCredHolder.sad.d, ecrCred.sad.d); + assert.equal(ecrCredHolder.sad.s, ECR_SCHEMA_SAID); + assert.equal(ecrCredHolder.sad.i, leAid.prefix); + assert.equal(ecrCredHolder.sad.a.i, roleAid.prefix); + assert.equal(ecrCredHolder.sad.e.le.n, leCred.sad.d); + assert.equal(ecrCredHolder.status.s, '0'); + assert(ecrCredHolder.atc !== undefined); + + console.log('Issuing ECR AUTH vLEI Credential'); + ecrAuthData.AID = roleAid.prefix; + const ecrAuthCredSource = Saider.saidify({ + d: '', + le: { + n: leCred.sad.d, + s: leCred.sad.s, + }, + })[1]; + + const ecrAuthCred = await getOrIssueCredential( + leClient, + leAid, + qviAid, + leRegistry, + ecrAuthData, + ECR_AUTH_SCHEMA_SAID, + ECR_AUTH_RULES, + ecrAuthCredSource + ); + + let ecrAuthCredHolder = await getReceivedCredential( + qviClient, + ecrAuthCred.sad.d + ); + + if (!ecrAuthCredHolder) { + await sendGrantMessage(leClient, leAid, qviAid, ecrAuthCred); + await sendAdmitMessage(qviClient, qviAid, leAid); + + ecrAuthCredHolder = await retry(async () => { + const cred = await getReceivedCredential( + qviClient, + ecrAuthCred.sad.d + ); + assert(cred !== undefined); + return cred; + }, CRED_RETRY_DEFAULTS); + } + + assert.equal(ecrAuthCredHolder.sad.d, ecrAuthCred.sad.d); + assert.equal(ecrAuthCredHolder.sad.s, ECR_AUTH_SCHEMA_SAID); + assert.equal(ecrAuthCredHolder.sad.i, leAid.prefix); + assert.equal(ecrAuthCredHolder.sad.a.i, qviAid.prefix); + assert.equal(ecrAuthCredHolder.sad.a.AID, roleAid.prefix); + assert.equal(ecrAuthCredHolder.sad.e.le.n, leCred.sad.d); + assert.equal(ecrAuthCredHolder.status.s, '0'); + assert(ecrAuthCredHolder.atc !== undefined); + + console.log('Issuing ECR vLEI Credential from ECR AUTH'); + const ecrCredSource2 = Saider.saidify({ + d: '', + auth: { + n: ecrAuthCred.sad.d, + s: ecrAuthCred.sad.s, + o: 'I2I', + }, + })[1]; + + const ecrCred2 = await getOrIssueCredential( + qviClient, + qviAid, + roleAid, + qviRegistry, + ecrData, + ECR_SCHEMA_SAID, + ECR_RULES, + ecrCredSource2, + true + ); + + let ecrCredHolder2 = await getReceivedCredential( + roleClient, + ecrCred2.sad.d + ); + + if (!ecrCredHolder2) { + await sendGrantMessage(qviClient, qviAid, roleAid, ecrCred2); + await sendAdmitMessage(roleClient, roleAid, qviAid); + + ecrCredHolder2 = await retry(async () => { + const cred = await getReceivedCredential( + roleClient, + ecrCred2.sad.d + ); + assert(cred !== undefined); + return cred; + }, CRED_RETRY_DEFAULTS); + } + + assert.equal(ecrCredHolder2.sad.d, ecrCred2.sad.d); + assert.equal(ecrCredHolder2.sad.s, ECR_SCHEMA_SAID); + assert.equal(ecrCredHolder2.sad.i, qviAid.prefix); + assert.equal(ecrCredHolder2.sad.e.auth.n, ecrAuthCred.sad.d); + assert.equal(ecrCredHolder2.status.s, '0'); + assert(ecrCredHolder2.atc !== undefined); + + console.log('Issuing OOR AUTH vLEI Credential'); + oorAuthData.AID = roleAid.prefix; + const oorAuthCredSource = Saider.saidify({ + d: '', + le: { + n: leCred.sad.d, + s: leCred.sad.s, + }, + })[1]; + + const oorAuthCred = await getOrIssueCredential( + leClient, + leAid, + qviAid, + leRegistry, + oorAuthData, + OOR_AUTH_SCHEMA_SAID, + OOR_AUTH_RULES, + oorAuthCredSource + ); + + let oorAuthCredHolder = await getReceivedCredential( + qviClient, + oorAuthCred.sad.d + ); + + if (!oorAuthCredHolder) { + await sendGrantMessage(leClient, leAid, qviAid, oorAuthCred); + await sendAdmitMessage(qviClient, qviAid, leAid); + + oorAuthCredHolder = await retry(async () => { + const cred = await getReceivedCredential( + qviClient, + oorAuthCred.sad.d + ); + assert(cred !== undefined); + return cred; + }, CRED_RETRY_DEFAULTS); + } + + assert.equal(oorAuthCredHolder.sad.d, oorAuthCred.sad.d); + assert.equal(oorAuthCredHolder.sad.s, OOR_AUTH_SCHEMA_SAID); + assert.equal(oorAuthCredHolder.sad.i, leAid.prefix); + assert.equal(oorAuthCredHolder.sad.a.i, qviAid.prefix); + assert.equal(oorAuthCredHolder.sad.a.AID, roleAid.prefix); + assert.equal(oorAuthCredHolder.sad.e.le.n, leCred.sad.d); + assert.equal(oorAuthCredHolder.status.s, '0'); + assert(oorAuthCredHolder.atc !== undefined); + + console.log('Issuing OOR vLEI Credential from OOR AUTH'); + const oorCredSource = Saider.saidify({ + d: '', + auth: { + n: oorAuthCred.sad.d, + s: oorAuthCred.sad.s, + o: 'I2I', + }, + })[1]; + + const oorCred = await getOrIssueCredential( + qviClient, + qviAid, + roleAid, + qviRegistry, + oorData, + OOR_SCHEMA_SAID, + OOR_RULES, + oorCredSource + ); + + let oorCredHolder = await getReceivedCredential(roleClient, oorCred.sad.d); + + if (!oorCredHolder) { + await sendGrantMessage(qviClient, qviAid, roleAid, oorCred); + await sendAdmitMessage(roleClient, roleAid, qviAid); + + oorCredHolder = await retry(async () => { + const cred = await getReceivedCredential(roleClient, oorCred.sad.d); + assert(cred !== undefined); + return cred; + }, CRED_RETRY_DEFAULTS); + } + + assert.equal(oorCredHolder.sad.d, oorCred.sad.d); + assert.equal(oorCredHolder.sad.s, OOR_SCHEMA_SAID); + assert.equal(oorCredHolder.sad.i, qviAid.prefix); + assert.equal(oorCredHolder.sad.e.auth.n, oorAuthCred.sad.d); + assert.equal(oorCredHolder.status.s, '0'); + assert(oorCredHolder.atc !== undefined); + + await assertOperations(gleifClient, qviClient, leClient, roleClient); + await warnNotifications(gleifClient, qviClient, leClient, roleClient); +}, 360000); + +async function getOrCreateRegistry( + client: SignifyClient, + aid: Aid, + registryName: string +): Promise<{ name: string; regk: string }> { + let registries = await client.registries().list(aid.name); + if (registries.length > 0) { + assert.equal(registries.length, 1); + } else { + const regResult = await client + .registries() + .create({ name: aid.name, registryName: registryName }); + await waitOperation(client, await regResult.op()); + + registries = await retry(async () => { + const result = await client.registries().list(aid.name); + assert(result.length >= 1, 'Expected at least one registry'); + return result; + }); + } + return registries[0]; +} + +async function sendGrantMessage( + senderClient: SignifyClient, + senderAid: Aid, + recipientAid: Aid, + credential: any +) { + const [grant, gsigs, gend] = await senderClient.ipex().grant({ + senderName: senderAid.name, + acdc: new Serder(credential.sad), + anc: new Serder(credential.anc), + iss: new Serder(credential.iss), + ancAttachment: credential.ancAttachment, + recipient: recipientAid.prefix, + datetime: createTimestamp(), + }); + + let op = await senderClient + .ipex() + .submitGrant(senderAid.name, grant, gsigs, gend, [recipientAid.prefix]); + op = await waitOperation(senderClient, op); +} + +async function sendAdmitMessage( + senderClient: SignifyClient, + senderAid: Aid, + recipientAid: Aid +) { + const notifications = await waitForNotifications( + senderClient, + '/exn/ipex/grant' + ); + assert.equal(notifications.length, 1); + const grantNotification = notifications[0]; + + const [admit, sigs, aend] = await senderClient.ipex().admit({ + senderName: senderAid.name, + message: '', + grantSaid: grantNotification.a.d!, + recipient: recipientAid.prefix, + datetime: createTimestamp(), + }); + + let op = await senderClient + .ipex() + .submitAdmit(senderAid.name, admit, sigs, aend, [recipientAid.prefix]); + op = await waitOperation(senderClient, op); + + await markAndRemoveNotification(senderClient, grantNotification); +} diff --git a/packages/signify-ts/test-integration/test-setup-clients.test.ts b/packages/signify-ts/test-integration/test-setup-clients.test.ts new file mode 100644 index 00000000..69bfa0ea --- /dev/null +++ b/packages/signify-ts/test-integration/test-setup-clients.test.ts @@ -0,0 +1,38 @@ +import { afterAll, assert, beforeAll, describe, test } from 'vitest'; +import { SignifyClient } from 'signify-ts'; +import { + assertOperations, + getOrCreateClients, + getOrCreateContact, + getOrCreateIdentifier, +} from './utils/test-util.ts'; + +let client1: SignifyClient, client2: SignifyClient; +let name1_id: string, name1_oobi: string; +let name2_id: string, name2_oobi: string; +let contact1_id: string, contact2_id: string; + +beforeAll(async () => { + // create two clients with random secrets + [client1, client2] = await getOrCreateClients(2); +}); +beforeAll(async () => { + [name1_id, name1_oobi] = await getOrCreateIdentifier(client1, 'name1'); + [name2_id, name2_oobi] = await getOrCreateIdentifier(client2, 'name2'); +}); +beforeAll(async () => { + contact1_id = await getOrCreateContact(client2, 'contact1', name1_oobi); + contact2_id = await getOrCreateContact(client1, 'contact2', name2_oobi); +}); +afterAll(async () => { + await assertOperations(client1, client2); +}); + +describe('test-setup-clients', () => { + test('step1', async () => { + assert.equal(name1_id, contact1_id); + }); + test('step2', async () => { + assert.equal(name2_id, contact2_id); + }); +}); diff --git a/packages/signify-ts/test-integration/test-setup-single-client.test.ts b/packages/signify-ts/test-integration/test-setup-single-client.test.ts new file mode 100644 index 00000000..cb8b0112 --- /dev/null +++ b/packages/signify-ts/test-integration/test-setup-single-client.test.ts @@ -0,0 +1,61 @@ +import { SignifyClient } from 'signify-ts'; +import { + assertOperations, + getOrCreateClients, + getOrCreateIdentifier, +} from './utils/test-util.ts'; +import { afterAll, assert, beforeAll, describe, test } from 'vitest'; + +let client: SignifyClient; +let name1_id: string, name1_oobi: string; + +beforeAll(async () => { + // Create client with pre-defined secret. Allows working with known identifiers + [client] = await getOrCreateClients(1, ['0ADF2TpptgqcDE5IQUF1HeTp']); + [name1_id, name1_oobi] = await getOrCreateIdentifier(client, 'name1'); +}); + +afterAll(async () => { + await assertOperations(client); +}); + +describe('test-setup-single-client', () => { + test('step1', async () => { + assert.equal( + client.controller?.pre, + 'EB3UGWwIMq7ppzcQ697ImQIuXlBG5jzh-baSx-YG3-tY' + ); + }); + + test('step2', async () => { + const oobi = await client.oobis().get('name1', 'witness'); + assert.strictEqual(oobi.oobis.length, 3); + assert.equal( + name1_oobi, + `http://127.0.0.1:3902/oobi/${name1_id}/agent/${client.agent?.pre}` + ); + assert.equal( + oobi.oobis[0], + `http://127.0.0.1:5642/oobi/${name1_id}/witness/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha` + ); + assert.equal( + oobi.oobis[1], + `http://127.0.0.1:5643/oobi/${name1_id}/witness/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM` + ); + assert.equal( + oobi.oobis[2], + `http://127.0.0.1:5644/oobi/${name1_id}/witness/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX` + ); + }); + + test('validate config', async () => { + const config = await client.config().get(); + assert.deepEqual(config, { + iurls: [ + 'http://127.0.0.1:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness', + 'http://127.0.0.1:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wil&tag=witness', + 'http://127.0.0.1:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wes&tag=witness', + ], + }); + }); +}); diff --git a/packages/signify-ts/test-integration/utils/multisig-utils.ts b/packages/signify-ts/test-integration/utils/multisig-utils.ts new file mode 100644 index 00000000..ba76000e --- /dev/null +++ b/packages/signify-ts/test-integration/utils/multisig-utils.ts @@ -0,0 +1,498 @@ +import signify, { + Algos, + CreateIdentiferArgs, + CredentialData, + Serder, + Siger, + SignifyClient, + d, + messagize, + HabState, +} from 'signify-ts'; +import { getStates, waitAndMarkNotification } from './test-util.ts'; +import assert from 'assert'; + +export interface AcceptMultisigInceptArgs { + groupName: string; + localMemberName: string; + msgSaid: string; +} + +export interface StartMultisigInceptArgs { + groupName: string; + localMemberName: string; + participants: string[]; + isith?: number | string | string[]; + nsith?: number | string | string[]; + toad?: number; + wits?: string[]; + delpre?: string; +} + +export async function acceptMultisigIncept( + client2: SignifyClient, + { groupName, localMemberName, msgSaid }: AcceptMultisigInceptArgs +) { + const memberHab = await client2.identifiers().get(localMemberName); + + const res = await client2.groups().getRequest(msgSaid); + const exn = res[0].exn; + const icp = exn.e.icp; + const smids = exn.a.smids; + const rmids = exn.a.rmids; + const states = await getStates(client2, smids); + const rstates = await getStates(client2, rmids); + + const icpResult2 = await client2.identifiers().create(groupName, { + algo: Algos.group, + mhab: memberHab, + isith: icp.kt, + nsith: icp.nt, + toad: parseInt(icp.bt), + wits: icp.b, + states: states, + rstates: rstates, + delpre: icp.di, + }); + const op2 = await icpResult2.op(); + const serder = icpResult2.serder; + const sigs = icpResult2.sigs; + const sigers = sigs.map((sig) => new Siger({ qb64: sig })); + + const ims = d(messagize(serder, sigers)); + const atc = ims.substring(serder.size); + const embeds = { + icp: [serder, atc], + }; + + const recipients = smids.filter((id: string) => memberHab.prefix !== id); + + client2 + .exchanges() + .send( + localMemberName, + groupName, + memberHab, + '/multisig/icp', + { gid: serder.pre, smids: smids, rmids: smids }, + embeds, + recipients + ); + + return op2; +} + +export async function addEndRoleMultisig( + client: SignifyClient, + groupName: string, + aid: HabState, + otherMembersAIDs: HabState[], + multisigAID: HabState, + timestamp: string, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/rpy'); + + const opList: any[] = []; + const members = await client.identifiers().members(multisigAID.name); + const signings = members['signing']; + + for (const signing of signings) { + const agentEnds = signing.ends.agent; + if (!agentEnds) { + throw new Error('signing.ends.agent is null or undefined'); + } + const eid = Object.keys(agentEnds)[0]; + const endRoleResult = await client + .identifiers() + .addEndRole(multisigAID.name, 'agent', eid, timestamp); + const op = await endRoleResult.op(); + opList.push(op); + + const rpy = endRoleResult.serder; + const sigs = endRoleResult.sigs; + const ghabState1 = multisigAID.state; + const seal = [ + 'SealEvent', + { + i: multisigAID.prefix, + s: ghabState1['ee']['s'], + d: ghabState1['ee']['d'], + }, + ]; + const sigers = sigs.map( + (sig: string) => new signify.Siger({ qb64: sig }) + ); + const roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + const atc = roleims.substring(rpy.size); + const roleembeds = { + rpy: [rpy, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + await client + .exchanges() + .send( + aid.name, + groupName, + aid, + '/multisig/rpy', + { gid: multisigAID.prefix }, + roleembeds, + recp + ); + } + + return opList; +} + +export async function admitMultisig( + client: SignifyClient, + aid: HabState, + otherMembersAIDs: HabState[], + multisigAID: HabState, + recipientAID: HabState, + timestamp: string + // numGrantMsgs: number +) { + const grantMsgSaid = await waitAndMarkNotification( + client, + '/exn/ipex/grant' + ); + + const [admit, sigs, end] = await client.ipex().admit({ + senderName: multisigAID.name, + message: '', + grantSaid: grantMsgSaid, + recipient: recipientAID.prefix, + datetime: timestamp, + }); + + await client + .ipex() + .submitAdmit(multisigAID.name, admit, sigs, end, [recipientAID.prefix]); + + const mstate = multisigAID.state; + const seal = [ + 'SealEvent', + { i: multisigAID.prefix, s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + const sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(admit, sigers, seal)); + let atc = ims.substring(admit.size); + atc += end; + const gembeds = { + exn: [admit, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/exn', + { gid: multisigAID.prefix }, + gembeds, + recp + ); +} + +export async function createAIDMultisig( + client: SignifyClient, + aid: HabState, + otherMembersAIDs: HabState[], + groupName: string, + kargs: CreateIdentiferArgs, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/icp'); + + const icpResult = await client.identifiers().create(groupName, kargs); + const op = await icpResult.op(); + + const serder = icpResult.serder; + const sigs = icpResult.sigs; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(serder, sigers)); + const atc = ims.substring(serder.size); + const embeds = { + icp: [serder, atc], + }; + const smids = kargs.states?.map((state) => state['i']); + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/icp', + { gid: serder.pre, smids: smids, rmids: smids }, + embeds, + recp + ); + + return op; +} + +export async function createRegistryMultisig( + client: SignifyClient, + aid: HabState, + otherMembersAIDs: HabState[], + multisigAID: HabState, + registryName: string, + nonce: string, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/vcp'); + + const vcpResult = await client.registries().create({ + name: multisigAID.name, + registryName: registryName, + nonce: nonce, + }); + const op = await vcpResult.op(); + + const serder = vcpResult.regser; + const anc = vcpResult.serder; + const sigs = vcpResult.sigs; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(anc, sigers)); + const atc = ims.substring(anc.size); + const regbeds = { + vcp: [serder, ''], + anc: [anc, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'registry', + aid, + '/multisig/vcp', + { gid: multisigAID.prefix }, + regbeds, + recp + ); + + return op; +} + +export async function delegateMultisig( + client: SignifyClient, + aid: HabState, + otherMembersAIDs: HabState[], + multisigAID: HabState, + anchor: { i: string; s: string; d: string }, + isInitiator: boolean = false +) { + if (!isInitiator) { + const msgSaid = await waitAndMarkNotification(client, '/multisig/ixn'); + console.log( + `${aid.name}(${aid.prefix}) received exchange message to join the interaction event` + ); + const res = await client.groups().getRequest(msgSaid); + const exn = res[0].exn; + const ixn = exn.e.ixn; + anchor = ixn.a[0]; + } + + // const {delResult, delOp} = await retry(async () => { + const delResult = await client + .delegations() + .approve(multisigAID.name, anchor); + const appOp = await delResult.op(); + console.log( + `Delegator ${aid.name}(${aid.prefix}) approved delegation for ${ + multisigAID.name + } with anchor ${JSON.stringify(anchor)}` + ); + + assert.equal( + JSON.stringify(delResult.serder.sad.a[0]), + JSON.stringify(anchor) + ); + + const serder = delResult.serder; + const sigs = delResult.sigs; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(serder, sigers)); + const atc = ims.substring(serder.size); + const xembeds = { + ixn: [serder, atc], + }; + const smids = [aid.prefix, ...otherMembersAIDs.map((aid) => aid.prefix)]; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + multisigAID.name, + aid, + '/multisig/ixn', + { gid: serder.pre, smids: smids, rmids: smids }, + xembeds, + recp + ); + + if (isInitiator) { + console.log( + `${aid.name}(${aid.prefix}) initiates delegation interaction event, waiting for others to join...` + ); + } else { + console.log(`${aid.name}(${aid.prefix}) joins interaction event`); + } + + return appOp; +} + +export async function grantMultisig( + client: SignifyClient, + aid: HabState, + otherMembersAIDs: HabState[], + multisigAID: HabState, + recipientAID: HabState, + credential: any, + timestamp: string, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/exn'); + + const [grant, sigs, end] = await client.ipex().grant({ + senderName: multisigAID.name, + acdc: new Serder(credential.sad), + anc: new Serder(credential.anc), + iss: new Serder(credential.iss), + recipient: recipientAID.prefix, + datetime: timestamp, + }); + + await client + .ipex() + .submitGrant(multisigAID.name, grant, sigs, end, [recipientAID.prefix]); + + const mstate = multisigAID.state; + const seal = [ + 'SealEvent', + { i: multisigAID.prefix, s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + const gims = signify.d(signify.messagize(grant, sigers, seal)); + let atc = gims.substring(grant.size); + atc += end; + const gembeds = { + exn: [grant, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/exn', + { gid: multisigAID.prefix }, + gembeds, + recp + ); +} + +export async function issueCredentialMultisig( + client: SignifyClient, + aid: HabState, + otherMembersAIDs: HabState[], + multisigAIDName: string, + kargsIss: CredentialData, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/iss'); + + const credResult = await client + .credentials() + .issue(multisigAIDName, kargsIss); + const op = credResult.op; + + const multisigAID = await client.identifiers().get(multisigAIDName); + const keeper = client.manager!.get(multisigAID); + const sigs = await keeper.sign(signify.b(credResult.anc.raw)); + const sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(credResult.anc, sigers)); + const atc = ims.substring(credResult.anc.size); + const embeds = { + acdc: [credResult.acdc, ''], + iss: [credResult.iss, ''], + anc: [credResult.anc, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/iss', + { gid: multisigAID.prefix }, + embeds, + recp + ); + + return op; +} + +export async function startMultisigIncept( + client: SignifyClient, + { + groupName, + localMemberName, + participants, + ...args + }: StartMultisigInceptArgs +) { + const aid1 = await client.identifiers().get(localMemberName); + const participantStates = await getStates(client, participants); + const icpResult1 = await client.identifiers().create(groupName, { + algo: Algos.group, + mhab: aid1, + isith: args.isith, + nsith: args.nsith, + toad: args.toad, + wits: args.wits, + delpre: args.delpre, + states: participantStates, + rstates: participantStates, + }); + const op1 = await icpResult1.op(); + const serder = icpResult1.serder; + + const sigs = icpResult1.sigs; + const sigers = sigs.map((sig) => new Siger({ qb64: sig })); + const ims = d(messagize(serder, sigers)); + const atc = ims.substring(serder.size); + const embeds = { + icp: [serder, atc], + }; + + const smids = participantStates.map((state) => state['i']); + + await client + .exchanges() + .send( + localMemberName, + groupName, + aid1, + '/multisig/icp', + { gid: serder.pre, smids: smids, rmids: smids }, + embeds, + participants + ); + return op1; +} diff --git a/packages/signify-ts/test-integration/utils/resolve-env.ts b/packages/signify-ts/test-integration/utils/resolve-env.ts new file mode 100644 index 00000000..0c745171 --- /dev/null +++ b/packages/signify-ts/test-integration/utils/resolve-env.ts @@ -0,0 +1,28 @@ +export interface TestEnvironment { + url: string; + bootUrl: string; + vleiServerUrl: string; + witnessUrls: string[]; + witnessIds: string[]; +} + +const WAN = 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha'; +const WIL = 'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM'; +const WES = 'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX'; + +export function resolveEnvironment(): TestEnvironment { + const url = 'http://127.0.0.1:3901'; + const bootUrl = 'http://127.0.0.1:3903'; + + return { + url, + bootUrl, + vleiServerUrl: 'http://127.0.0.1:7723', + witnessUrls: [ + 'http://127.0.0.1:5642', + 'http://127.0.0.1:5643', + 'http://127.0.0.1:5644', + ], + witnessIds: [WAN, WIL, WES], + }; +} diff --git a/packages/signify-ts/test-integration/utils/retry.ts b/packages/signify-ts/test-integration/utils/retry.ts new file mode 100644 index 00000000..4af84450 --- /dev/null +++ b/packages/signify-ts/test-integration/utils/retry.ts @@ -0,0 +1,53 @@ +import { setTimeout } from 'timers/promises'; + +export interface RetryOptions { + maxSleep?: number; + minSleep?: number; + maxRetries?: number; + timeout?: number; + signal?: AbortSignal; +} + +export async function retry( + fn: () => Promise, + options: RetryOptions = {} +): Promise { + const { + maxSleep = 1000, + minSleep = 10, + maxRetries, + timeout = 10000, + } = options; + + const increaseFactor = 50; + + let retries = 0; + let cause: Error | null = null; + const start = Date.now(); + + while ( + (options.signal === undefined || options.signal.aborted === false) && + Date.now() - start < timeout && + (maxRetries === undefined || retries < maxRetries) + ) { + try { + const result = await fn(); + return result; + } catch (err) { + cause = err as Error; + const delay = Math.max( + minSleep, + Math.min(maxSleep, 2 ** retries * increaseFactor) + ); + retries++; + await setTimeout(delay, undefined, { signal: options.signal }); + } + } + + if (!cause) { + cause = new Error(`Failed after ${retries} attempts`); + } + + Object.assign(cause, { retries, maxAttempts: maxRetries }); + throw cause; +} diff --git a/packages/signify-ts/test-integration/utils/test-step.ts b/packages/signify-ts/test-integration/utils/test-step.ts new file mode 100644 index 00000000..4698b117 --- /dev/null +++ b/packages/signify-ts/test-integration/utils/test-step.ts @@ -0,0 +1,28 @@ +/** + * Provides a way to group logically related test steps in an integration test + * + * Can be useful to provide logging when a step succeeds, or to be able to use + * locally scoped variables. + * + * In long tests it can also be useful to create visual groups. + * @param description + * @param fn + * @returns + */ +export async function step( + description: string, + fn: () => Promise +): Promise { + try { + const start = Date.now(); + const response = await fn(); + + // Bypassing console.log to avoid the verbose log output from test runner + process.stdout.write( + `Step - ${description} - finished (${Date.now() - start}ms)\n` + ); + return response; + } catch (error) { + throw new Error(`Step - ${description} - failed`, { cause: error }); + } +} diff --git a/packages/signify-ts/test-integration/utils/test-util.ts b/packages/signify-ts/test-integration/utils/test-util.ts new file mode 100644 index 00000000..39b4360e --- /dev/null +++ b/packages/signify-ts/test-integration/utils/test-util.ts @@ -0,0 +1,530 @@ +import signify, { + CreateIdentiferArgs, + EventResult, + Operation, + randomPasscode, + ready, + Salter, + SignifyClient, + Tier, + HabState, + ExternalModule, +} from 'signify-ts'; +import { RetryOptions, retry } from './retry.ts'; +import assert from 'assert'; +import { resolveEnvironment } from './resolve-env.ts'; +import { expect } from 'vitest'; + +export interface Aid { + name: string; + prefix: string; + oobi: string; +} + +export interface Notification { + i: string; + dt: string; + r: boolean; + a: { r: string; d?: string; m?: string }; +} + +export function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +export async function admitSinglesig( + client: SignifyClient, + aidName: string, + recipientAid: HabState +) { + const grantMsgSaid = await waitAndMarkNotification( + client, + '/exn/ipex/grant' + ); + + const [admit, sigs, aend] = await client.ipex().admit({ + senderName: aidName, + message: '', + grantSaid: grantMsgSaid, + recipient: recipientAid.prefix, + }); + + await client + .ipex() + .submitAdmit(aidName, admit, sigs, aend, [recipientAid.prefix]); +} + +/** + * Assert that all operations were waited for. + *

This is a postcondition check to make sure all long-running operations have been waited for + * @see waitOperation + */ +export async function assertOperations( + ...clients: SignifyClient[] +): Promise { + for (const client of clients) { + const operations = await client.operations().list(); + assert.strictEqual(operations.length, 0); + } +} + +/** + * Assert that all notifications were handled. + *

This is a postcondition check to make sure all notifications have been handled + * @see markNotification + * @see markAndRemoveNotification + */ +export async function assertNotifications( + ...clients: SignifyClient[] +): Promise { + for (const client of clients) { + const res = await client.notifications().list(); + const notes = res.notes.filter((i: { r: boolean }) => i.r === false); + assert.strictEqual(notes.length, 0); + } +} + +export async function createAid( + client: SignifyClient, + name: string +): Promise { + const [prefix, oobi] = await getOrCreateIdentifier(client, name); + return { prefix, oobi, name }; +} + +export async function createAID(client: signify.SignifyClient, name: string) { + await getOrCreateIdentifier(client, name); + const aid = await client.identifiers().get(name); + console.log(name, 'AID:', aid.prefix); + return aid; +} + +export function createTimestamp() { + return new Date().toISOString().replace('Z', '000+00:00'); +} + +/** + * Get list of end role authorizations for a Keri idenfitier + */ +export async function getEndRoles( + client: SignifyClient, + alias: string, + role?: string +): Promise { + const path = + role !== undefined + ? `/identifiers/${alias}/endroles/${role}` + : `/identifiers/${alias}/endroles`; + const response: Response = await client.fetch(path, 'GET', null); + if (!response.ok) throw new Error(await response.text()); + const result = await response.json(); + // console.log("getEndRoles", result); + return result; +} + +export async function getIssuedCredential( + issuerClient: SignifyClient, + issuerAID: HabState, + recipientAID: HabState, + schemaSAID: string +) { + const credentialList = await issuerClient.credentials().list({ + filter: { + '-i': issuerAID.prefix, + '-s': schemaSAID, + '-a-i': recipientAID.prefix, + }, + }); + assert(credentialList.length <= 1); + return credentialList[0]; +} + +export async function getOrCreateAID( + client: SignifyClient, + name: string, + kargs: CreateIdentiferArgs +): Promise { + try { + return await client.identifiers().get(name); + } catch { + const result: EventResult = await client + .identifiers() + .create(name, kargs); + + await waitOperation(client, await result.op()); + const aid = await client.identifiers().get(name); + + const op = await client + .identifiers() + .addEndRole(name, 'agent', client!.agent!.pre); + await waitOperation(client, await op.op()); + console.log(name, 'AID:', aid.prefix); + return aid; + } +} + +/** + * Connect or boot a SignifyClient instance + */ +export async function getOrCreateClient( + bran: string | undefined = undefined, + externalModule: ExternalModule[] = [] +): Promise { + const env = resolveEnvironment(); + await ready(); + bran ??= randomPasscode(); + bran = bran.padEnd(21, '_'); + const client = new SignifyClient( + env.url, + bran, + Tier.low, + env.bootUrl, + externalModule + ); + try { + await client.connect(); + } catch { + const res = await client.boot(); + if (!res.ok) throw new Error(); + await client.connect(); + } + console.log('client', { + agent: client.agent?.pre, + controller: client.controller.pre, + }); + return client; +} + +/** + * Connect or boot a number of SignifyClient instances + * @example + * Create two clients with random secrets + * let client1: SignifyClient, client2: SignifyClient; + * beforeAll(async () => { + * [client1, client2] = await getOrCreateClients(2); + * }); + * @example + * Launch test from shell with pre-defined secrets + * $ SIGNIFY_SECRETS="0ACqshJKkJ7DDXcaDuwnmI8s,0ABqicvyicXGvIVg6Ih-dngE" npm run test:integration + */ +export async function getOrCreateClients( + count: number, + brans: string[] | undefined = undefined +): Promise { + const tasks: Promise[] = []; + const secrets = process.env['SIGNIFY_SECRETS']?.split(','); + for (let i = 0; i < count; i++) { + tasks.push( + getOrCreateClient(brans?.at(i) ?? secrets?.at(i) ?? undefined) + ); + } + const clients: SignifyClient[] = await Promise.all(tasks); + console.log(`SIGNIFY_SECRETS="${clients.map((i) => i.bran).join(',')}"`); + return clients; +} + +/** + * Get or resolve a Keri contact + * @example + * Create a Keri contact before running tests + * let contact1_id: string; + * beforeAll(async () => { + * contact1_id = await getOrCreateContact(client2, "contact1", name1_oobi); + * }); + */ +export async function getOrCreateContact( + client: SignifyClient, + name: string, + oobi: string +): Promise { + const list = await client.contacts().list(undefined, 'alias', `^${name}$`); + // console.log("contacts.list", list); + if (list.length > 0) { + const contact = list[0]; + if (contact.oobi === oobi) { + // console.log("contacts.id", contact.id); + return contact.id; + } + } + let op = await client.oobis().resolve(oobi, name); + op = await waitOperation(client, op); + return op.response.i; +} + +/** + * Get or create a Keri identifier. Uses default witness config from `resolveEnvironment` + * @example + * Create a Keri identifier before running tests + * let name1_id: string, name1_oobi: string; + * beforeAll(async () => { + * [name1_id, name1_oobi] = await getOrCreateIdentifier(client1, "name1"); + * }); + * @see resolveEnvironment + */ +export async function getOrCreateIdentifier( + client: SignifyClient, + name: string, + kargs: CreateIdentiferArgs | undefined = undefined +): Promise<[string, string]> { + let id: any = undefined; + try { + const identfier = await client.identifiers().get(name); + // console.log("identifiers.get", identfier); + id = identfier.prefix; + } catch { + const env = resolveEnvironment(); + kargs ??= { + toad: env.witnessIds.length, + wits: env.witnessIds, + }; + const result: EventResult = await client + .identifiers() + .create(name, kargs); + let op = await result.op(); + op = await waitOperation(client, op); + // console.log("identifiers.create", op); + id = op.response.i; + } + const eid = client.agent?.pre!; + if (!(await hasEndRole(client, name, 'agent', eid))) { + const result: EventResult = await client + .identifiers() + .addEndRole(name, 'agent', eid); + let op = await result.op(); + op = await waitOperation(client, op); + console.log('identifiers.addEndRole', op); + } + + const oobi = await client.oobis().get(name, 'agent'); + const result: [string, string] = [id, oobi.oobis[0]]; + console.log(name, result); + return result; +} + +export async function getOrIssueCredential( + issuerClient: SignifyClient, + issuerAid: Aid, + recipientAid: Aid, + issuerRegistry: { regk: string }, + credData: any, + schema: string, + rules?: any, + source?: any, + privacy = false +): Promise { + const credentialList = await issuerClient.credentials().list(); + + if (credentialList.length > 0) { + const credential = credentialList.find( + (cred: any) => + cred.sad.s === schema && + cred.sad.i === issuerAid.prefix && + cred.sad.a.i === recipientAid.prefix + ); + if (credential) return credential; + } + + const issResult = await issuerClient.credentials().issue(issuerAid.name, { + ri: issuerRegistry.regk, + s: schema, + u: privacy ? new Salter({}).qb64 : undefined, + a: { + i: recipientAid.prefix, + u: privacy ? new Salter({}).qb64 : undefined, + ...credData, + }, + r: rules, + e: source, + }); + + await waitOperation(issuerClient, issResult.op); + const credential = await issuerClient + .credentials() + .get(issResult.acdc.sad.d); + + return credential; +} + +export async function getStates(client: SignifyClient, prefixes: string[]) { + const participantStates = await Promise.all( + prefixes.map((p) => client.keyStates().get(p)) + ); + return participantStates.map((s) => s[0]); +} + +/** + * Test if end role is authorized for a Keri identifier + */ +export async function hasEndRole( + client: SignifyClient, + alias: string, + role: string, + eid: string +): Promise { + const list = await getEndRoles(client, alias, role); + for (const i of list) { + if (i.role === role && i.eid === eid) { + return true; + } + } + return false; +} + +/** + * Logs a warning for each un-handled notification. + *

Replace warnNotifications with assertNotifications when test handles all notifications + * @see assertNotifications + */ +export async function warnNotifications( + ...clients: SignifyClient[] +): Promise { + let count = 0; + for (const client of clients) { + const res = await client.notifications().list(); + const notes = res.notes.filter((i: { r: boolean }) => i.r === false); + if (notes.length > 0) { + count += notes.length; + console.warn('notifications', notes); + } + } + expect(count).toBeGreaterThan(0); // replace warnNotifications with assertNotifications +} + +async function deleteOperations( + client: SignifyClient, + op: Operation +) { + if (op.metadata?.depends) { + await deleteOperations(client, op.metadata.depends); + } + + await client.operations().delete(op.name); +} + +export async function getReceivedCredential( + client: SignifyClient, + credId: string +): Promise { + const credentialList = await client.credentials().list({ + filter: { + '-d': credId, + }, + }); + let credential: any; + if (credentialList.length > 0) { + assert.equal(credentialList.length, 1); + credential = credentialList[0]; + } + return credential; +} + +/** + * Mark and remove notification. + */ +export async function markAndRemoveNotification( + client: SignifyClient, + note: Notification +): Promise { + try { + await client.notifications().mark(note.i); + } finally { + await client.notifications().delete(note.i); + } +} + +/** + * Mark notification as read. + */ +export async function markNotification( + client: SignifyClient, + note: Notification +): Promise { + await client.notifications().mark(note.i); +} + +export async function resolveOobi( + client: SignifyClient, + oobi: string, + alias?: string +) { + const op = await client.oobis().resolve(oobi, alias); + await waitOperation(client, op); +} + +export async function waitForCredential( + client: SignifyClient, + credSAID: string, + MAX_RETRIES: number = 10 +) { + let retryCount = 0; + while (retryCount < MAX_RETRIES) { + const cred = await getReceivedCredential(client, credSAID); + if (cred) return cred; + + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log(` retry-${retryCount}: No credentials yet...`); + retryCount = retryCount + 1; + } + throw Error('Credential SAID: ' + credSAID + ' has not been received'); +} + +export async function waitAndMarkNotification( + client: SignifyClient, + route: string, + options: RetryOptions = {} +) { + const notes = await waitForNotifications(client, route, options); + + await Promise.all( + notes.map(async (note) => { + await markNotification(client, note); + }) + ); + + return notes[notes.length - 1]?.a.d ?? ''; +} + +export async function waitForNotifications( + client: SignifyClient, + route: string, + options: RetryOptions = {} +): Promise { + return retry(async () => { + const response: { notes: Notification[] } = await client + .notifications() + .list(); + + const notes = response.notes.filter( + (note) => note.a.r === route && note.r === false + ); + + if (!notes.length) { + throw new Error(`No notifications with route ${route}`); + } + + return notes; + }, options); +} + +/** + * Poll for operation to become completed. + * Removes completed operation + */ +export async function waitOperation( + client: SignifyClient, + op: Operation | string, + signal?: AbortSignal +): Promise> { + if (typeof op === 'string') { + op = await client.operations().get(op); + } + + op = await client + .operations() + .wait(op, { signal: signal ?? AbortSignal.timeout(30000) }); + await deleteOperations(client, op); + + return op; +} diff --git a/packages/signify-ts/test-integration/witness.test.ts b/packages/signify-ts/test-integration/witness.test.ts new file mode 100644 index 00000000..2dc22025 --- /dev/null +++ b/packages/signify-ts/test-integration/witness.test.ts @@ -0,0 +1,56 @@ +// This scrip also work if you start keria with no config file with witness urls +import { assert, test } from 'vitest'; +import { resolveEnvironment } from './utils/resolve-env.ts'; +import { + getOrCreateClient, + resolveOobi, + waitOperation, +} from './utils/test-util.ts'; + +const WITNESS_AID = 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha'; +const { witnessUrls } = resolveEnvironment(); + +test('test witness', async () => { + const client1 = await getOrCreateClient(); + + // Client 1 resolves witness OOBI + await resolveOobi(client1, witnessUrls[0] + `/oobi/${WITNESS_AID}`, 'wit'); + console.log('Witness OOBI resolved'); + + // Client 1 creates AID with 1 witness + let icpResult1 = await client1.identifiers().create('aid1', { + toad: 1, + wits: [WITNESS_AID], + }); + await waitOperation(client1, await icpResult1.op()); + let aid1 = await client1.identifiers().get('aid1'); + console.log('AID:', aid1.prefix); + assert.equal(aid1.state.b.length, 1); + assert.equal(aid1.state.b[0], WITNESS_AID); + + icpResult1 = await client1.identifiers().rotate('aid1'); + await waitOperation(client1, await icpResult1.op()); + aid1 = await client1.identifiers().get('aid1'); + assert.equal(aid1.state.b.length, 1); + assert.equal(aid1.state.b[0], WITNESS_AID); + + // Remove witness + icpResult1 = await client1 + .identifiers() + .rotate('aid1', { cuts: [WITNESS_AID] }); + await waitOperation(client1, await icpResult1.op()); + aid1 = await client1.identifiers().get('aid1'); + assert.equal(aid1.state.b.length, 0); + + // Add witness again + + icpResult1 = await client1 + .identifiers() + .rotate('aid1', { adds: [WITNESS_AID] }); + + await waitOperation(client1, await icpResult1.op()); + aid1 = await client1.identifiers().get('aid1'); + assert.equal(aid1.state.b.length, 1); + assert.equal(aid1.state.b.length, 1); + assert.equal(aid1.state.b[0], WITNESS_AID); +}, 60000); diff --git a/packages/signify-ts/test/app/aiding.test.ts b/packages/signify-ts/test/app/aiding.test.ts new file mode 100644 index 00000000..bcc7527b --- /dev/null +++ b/packages/signify-ts/test/app/aiding.test.ts @@ -0,0 +1,509 @@ +import { assert, describe, it, expect, beforeEach, vitest } from 'vitest'; +import { + CreateIdentiferArgs, + RotateIdentifierArgs, +} from '../../src/keri/app/aiding.ts'; +import { Algos } from '../../src/keri/core/manager.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { randomUUID } from 'node:crypto'; +import { + Controller, + Identifier, + IdentifierDeps, + IdentifierManagerFactory, + randomPasscode, + Tier, +} from '../../src/index.ts'; +import { createMockIdentifierState } from './test-utils.ts'; + +const bran = '0123456789abcdefghijk'; + +export class MockClient implements IdentifierDeps { + manager: IdentifierManagerFactory; + controller: Controller; + pidx = 0; + + fetch = vitest.fn(); + + constructor(bran: string) { + this.controller = new Controller(bran, Tier.low); + this.manager = new IdentifierManagerFactory(this.controller.salter); + } + + identifiers() { + return new Identifier(this); + } + + getLastMockRequest() { + const [pathname, method, body] = this.fetch.mock.lastCall ?? []; + + return { + path: pathname, + method: method, + body: body, + }; + } +} + +let client: MockClient; +beforeEach(async () => { + await libsodium.ready; + client = new MockClient(bran); +}); + +describe('Aiding', () => { + it('Can list identifiers', async () => { + client.fetch.mockResolvedValue(Response.json({})); + await client.identifiers().list(); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers'); + assert.equal(lastCall.method, 'GET'); + }); + + it('Can create salty identifiers', async () => { + client.fetch.mockResolvedValue(Response.json({})); + await client + .identifiers() + .create('aid1', { bran: '0123456789abcdefghijk' }); + + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers'); + assert.equal(lastCall.method, 'POST'); + assert.equal(lastCall.body.name, 'aid1'); + assert.deepEqual(lastCall.body.icp, { + v: 'KERI10JSON00012b_', + t: 'icp', + d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: '0', + kt: '1', + k: ['DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9'], + nt: '1', + n: ['EAORnRtObOgNiOlMolji-KijC_isa3lRDpHCsol79cOc'], + bt: '0', + b: [], + c: [], + a: [], + }); + assert.deepEqual(lastCall.body.sigs, [ + 'AACZZe75PvUZ1lCREPxFAcX59XHo-BGMYTAGni-I4E0eqKznrEoK2d-mtWmWHwKns7tfnjOzTfDUcv7PLFJ52g0A', + ]); + assert.deepEqual(lastCall.body.salty.pidx, 0); + assert.deepEqual(lastCall.body.salty.kidx, 0); + assert.deepEqual(lastCall.body.salty.stem, 'signify:aid'); + assert.deepEqual(lastCall.body.salty.tier, 'low'); + assert.deepEqual(lastCall.body.salty.icodes, ['A']); + assert.deepEqual(lastCall.body.salty.ncodes, ['A']); + assert.deepEqual(lastCall.body.salty.dcode, 'E'); + assert.deepEqual(lastCall.body.salty.transferable, true); + }); + + it('Can create non-transferable salty identifiers', async () => { + client.fetch.mockResolvedValue(Response.json({})); + await client.identifiers().create('aid1', { + bran: '0123456789abcdefghijk', + transferable: false, + }); + + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers'); + assert.equal(lastCall.method, 'POST'); + assert.equal(lastCall.body.name, 'aid1'); + assert.deepEqual(lastCall.body.icp, { + v: 'KERI10JSON0000fd_', + t: 'icp', + d: 'EFI3s8I7M6b8iiOFJOqDfjuak9NQJtVx8N2Px_cm2lsN', + i: 'BPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9', + s: '0', + kt: '1', + k: ['BPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9'], + nt: '0', + n: [], + bt: '0', + b: [], + c: [], + a: [], + }); + assert.deepEqual(lastCall.body.sigs, [ + 'AAD43ke-FKLzD_eOJuE7-G5jeqjs9iyirE-atbxd4sSvEn-0fRibOWI5jtvlE8b8Dn8_rVRa1BUCyDfmEXzy1uwA', + ]); + assert.deepEqual(lastCall.body.salty.pidx, 0); + assert.deepEqual(lastCall.body.salty.kidx, 0); + assert.deepEqual(lastCall.body.salty.stem, 'signify:aid'); + assert.deepEqual(lastCall.body.salty.tier, 'low'); + assert.deepEqual(lastCall.body.salty.icodes, ['A']); + assert.deepEqual(lastCall.body.salty.dcode, 'B'); + assert.deepEqual(lastCall.body.salty.transferable, false); + }); + + it('Can get identifiers with special characters in the name', async () => { + client.fetch.mockResolvedValue(Response.json({})); + await client.identifiers().get('a name with ñ!'); + + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.method, 'GET'); + assert.equal(lastCall.path, '/identifiers/a%20name%20with%20%C3%B1!'); + }); + + it('Can create salty AID with multiple signatures', async () => { + client.fetch.mockResolvedValue(Response.json({})); + + const result = await client.identifiers().create('aid2', { + count: 3, + ncount: 3, + isith: '2', + nsith: '2', + bran: '0123456789lmnopqrstuv', + }); + + await result.op(); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers'); + assert.equal(lastCall.method, 'POST'); + assert.equal(lastCall.body.name, 'aid2'); + assert.deepEqual(lastCall.body.icp, { + v: 'KERI10JSON0001e7_', + t: 'icp', + d: 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX', + i: 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX', + s: '0', + kt: '2', + k: [ + 'DGBw7C7AfC7jbD3jLLRS3SzIWFndM947TyNWKQ52iQx5', + 'DD_bHYFsgWXuCbz3SD0HjCIe_ITjRvEoCGuZ4PcNFFDz', + 'DEe9u8k0fm1wMFAuOIsCtCNrpduoaV5R21rAcJl0awze', + ], + nt: '2', + n: [ + 'EML5FrjCpz8SEl4dh0U15l8bMRhV_O5iDcR1opLJGBSH', + 'EJpKquuibYTqpwMDqEFAFs0gwq0PASAHZ_iDmSF3I2Vg', + 'ELplTAiEKdobFhlf-dh1vUb2iVDW0dYOSzs1dR7fQo60', + ], + bt: '0', + b: [], + c: [], + a: [], + }); + assert.deepEqual(lastCall.body.sigs, [ + 'AAD9_IgPaUEBjAl1Ck61Jkn78ErzsnVkIxpaFBYSdSEAW4NbtXsLiUn1olijzdTQYn_Byq6MaEk-eoMN3Oc0WEEC', + 'ABBWJ7KkAXXiRK8JyEUpeARHJTTzlBHu_ev-jUrNEhV9sX4_4lI7wxowrQisumt5r50bUNfYBK7pxSwHk8I4IFQP', + 'ACDTITaEquHdYKkS-94tVCxL3IYrtvhlTt__sSUavTJT6fI3KB-uwXV7L0SfzMq0gFqYxkheH2LdC4HkAW2mH4QJ', + ]); + assert.deepEqual(lastCall.body.salty.pidx, 0); + assert.deepEqual(lastCall.body.salty.kidx, 0); + assert.deepEqual(lastCall.body.salty.stem, 'signify:aid'); + assert.deepEqual(lastCall.body.salty.tier, 'low'); + assert.deepEqual(lastCall.body.salty.icodes, ['A', 'A', 'A']); + assert.deepEqual(lastCall.body.salty.ncodes, ['A', 'A', 'A']); + assert.deepEqual(lastCall.body.salty.dcode, 'E'); + assert.deepEqual(lastCall.body.salty.transferable, true); + }); + + it('Should throw error if fetch call fails when creating identifier', async () => { + const error = new Error(`Fail ${randomUUID()}`); + client.fetch.mockRejectedValue(error); + await expect( + client + .identifiers() + .create('aid1', { bran: '0123456789abcdefghijk' }) + ).rejects.toThrow(error); + }); + + it('Can rotate salty identifier', async () => { + const aid1 = await createMockIdentifierState('aid1', bran, {}); + client.fetch.mockResolvedValueOnce(Response.json(aid1)); + client.fetch.mockResolvedValueOnce(Response.json({})); + + await client.identifiers().rotate('aid1'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers/aid1/events'); + assert.equal(lastCall.method, 'POST'); + assert.deepEqual(lastCall.body.rot, { + v: 'KERI10JSON000160_', + t: 'rot', + d: 'EBQABdRgaxJONrSLcgrdtbASflkvLxJkiDO0H-XmuhGg', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: '1', + p: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + kt: '1', + k: ['DHgomzINlGJHr-XP3sv2ZcR9QsIEYS3LJhs4KRaZYKly'], + nt: '1', + n: ['EJMovBlrBuD6BVeUsGSxLjczbLEbZU9YnTSud9K4nVzk'], + bt: '0', + br: [], + ba: [], + a: [], + }); + assert.deepEqual(lastCall.body.sigs, [ + 'AABWSckRpAWLpfFSrpnDR3SzQASrRSVKGh8AnHxauhN_43qKkqPb9l04utnTm2ixNpGGJ-UB8qdKMjfkEQ61AIQC', + ]); + assert.deepEqual(lastCall.body.salty.pidx, 0); + assert.deepEqual(lastCall.body.salty.kidx, 1); + assert.deepEqual(lastCall.body.salty.stem, 'signify:aid'); + assert.deepEqual(lastCall.body.salty.tier, 'low'); + assert.deepEqual([...lastCall.body.salty.icodes], ['A']); + assert.deepEqual([...lastCall.body.salty.ncodes], ['A']); + assert.deepEqual(lastCall.body.salty.dcode, 'E'); + assert.deepEqual(lastCall.body.salty.transferable, true); + }); + + it('Can rotate salty identifier with sn > 10', async () => { + const aid1 = await createMockIdentifierState('aid1', bran, {}); + client.fetch.mockResolvedValueOnce( + Response.json({ + ...aid1, + state: { + ...aid1.state, + s: 'a', + }, + }) + ); + client.fetch.mockResolvedValueOnce(Response.json({})); + + await client.identifiers().rotate('aid1'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers/aid1/events'); + assert.equal(lastCall.method, 'POST'); + expect(lastCall.body.rot).toMatchObject({ + v: 'KERI10JSON000160_', + t: 'rot', + s: 'b', + }); + }); + + it('Can create interact event', async () => { + const data = [ + { + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: 0, + d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + }, + ]; + + const aid1 = await createMockIdentifierState('aid1', bran); + client.fetch.mockResolvedValueOnce(Response.json(aid1)); + client.fetch.mockResolvedValueOnce(Response.json({})); + + await client.identifiers().interact('aid1', data); + + const lastCall = client.getLastMockRequest(); + + assert.equal(lastCall.path, '/identifiers/aid1/events'); + assert.equal(lastCall.method, 'POST'); + expect(lastCall.body.ixn).toMatchObject({ + v: 'KERI10JSON000138_', + t: 'ixn', + d: 'EPtNJLDft3CB-oz3qIhe86fnTKs-GYWiWyx8fJv3VO5e', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: '1', + p: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + a: data, + }); + + assert.deepEqual(lastCall.body.sigs, [ + 'AADEzKk-5LT6vH-PWFb_1i1A8FW-KGHORtTOCZrKF4gtWkCr9vN1z_mDSVKRc6MKktpdeB3Ub1fWCGpnS50hRgoJ', + ]); + }); + + it('Can create interact event when sequence number > 10', async () => { + const data = [ + { + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: 0, + d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + }, + ]; + + const aid1 = await createMockIdentifierState('aid1', bran); + client.fetch.mockResolvedValueOnce( + Response.json({ + ...aid1, + state: { + ...aid1.state, + s: 'a', + }, + }) + ); + client.fetch.mockResolvedValueOnce(Response.json({})); + + await client.identifiers().interact('aid1', data); + + const lastCall = client.getLastMockRequest(); + + assert.equal(lastCall.path, '/identifiers/aid1/events'); + assert.equal(lastCall.method, 'POST'); + expect(lastCall.body.ixn).toMatchObject({ + s: 'b', + a: data, + }); + }); + + it('Can add end role', async () => { + const aid1 = await createMockIdentifierState('aid1', bran, {}); + client.fetch.mockResolvedValueOnce(Response.json(aid1)); + client.fetch.mockResolvedValueOnce(Response.json({})); + + await client.identifiers().addEndRole('aid1', 'agent'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers/aid1/endroles'); + assert.equal(lastCall.method, 'POST'); + assert.equal(lastCall.body.rpy.t, 'rpy'); + assert.equal(lastCall.body.rpy.r, '/end/role/add'); + assert.deepEqual(lastCall.body.rpy.a, { + cid: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + role: 'agent', + }); + }); + + it('Should throw error if fetch call fails when adding end role', async () => { + const error = new Error(`Fail ${randomUUID()}`); + client.fetch.mockRejectedValue(error); + await expect( + client.identifiers().addEndRole('aid1', 'agent') + ).rejects.toThrow(error); + }); + + it('Can get members', async () => { + client.fetch.mockResolvedValue(Response.json({})); + await client.identifiers().members('aid1'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers/aid1/members'); + assert.equal(lastCall.method, 'GET'); + }); + + it('Randy identifiers', async () => { + client.fetch.mockResolvedValue(Response.json({})); + await client.identifiers().create('aid1', { + bran: '0123456789abcdefghijk', + algo: Algos.randy, + }); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers'); + assert.equal(lastCall.method, 'POST'); + assert.equal(lastCall.body.name, 'aid1'); + assert.deepEqual(lastCall.body.icp.s, '0'); + assert.deepEqual(lastCall.body.icp.kt, '1'); + assert.deepEqual(lastCall.body.randy.transferable, true); + }); + + it('Can rename identifier', async () => { + client.fetch.mockResolvedValue(Response.json({})); + await client.identifiers().update('aid1', { name: 'aid2' }); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers/aid1'); + assert.equal(lastCall.method, 'PUT'); + assert.equal(lastCall.body.name, 'aid2'); + }); + + describe('Group identifiers', () => { + it('Can Rotate group', async () => { + const member1 = await createMockIdentifierState( + randomUUID(), + bran, + {} + ); + const member2 = await createMockIdentifierState( + randomUUID(), + randomPasscode(), + {} + ); + + const group = await createMockIdentifierState(randomUUID(), bran, { + algo: Algos.group, + mhab: member1, + nsith: '1', + isith: '1', + states: [member1.state, member2.state], + rstates: [member1.state, member2.state], + }); + + client.fetch.mockResolvedValueOnce( + Response.json(group, { status: 200 }) + ); + client.fetch.mockResolvedValueOnce( + Response.json({}, { status: 202 }) + ); + + const args: RotateIdentifierArgs = { + nsith: '1', + states: [member1.state, member2.state], + rstates: [member1.state, member2.state], + }; + + await client.identifiers().rotate(group.name, args); + const request = client.getLastMockRequest(); + const body = request.body; + expect(body).toMatchObject({ + rot: { + t: 'rot', + }, + }); + }); + + it('Should use the previous sign threshold as the default next threshold', async () => { + const member1 = await createMockIdentifierState(randomUUID(), bran); + const member2 = await createMockIdentifierState( + randomUUID(), + randomPasscode() + ); + + const nextThreshold = ['1/2', '1/2']; + const group = await createMockIdentifierState(randomUUID(), bran, { + algo: Algos.group, + mhab: member1, + isith: ['1/3', '2/3'], + nsith: nextThreshold, + states: [member1.state, member2.state], + rstates: [member1.state, member2.state], + }); + + client.fetch.mockResolvedValueOnce(Response.json(group)); + client.fetch.mockResolvedValueOnce(Response.json({})); + await client.identifiers().rotate(group.name, { + nsith: '1', + states: [member1.state, member2.state], + rstates: [member1.state, member2.state], + }); + const request = client.getLastMockRequest(); + expect(request.body.rot).toMatchObject({ + t: 'rot', + kt: nextThreshold, + }); + }); + }); + + describe('Typings test', () => { + it('CreateIdentiferArgs', () => { + let args: CreateIdentiferArgs; + args = { + isith: 1, + nsith: 1, + }; + args = { + isith: '1', + nsith: '1', + }; + args = { + isith: ['1'], + nsith: ['1'], + }; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + args !== null; // avoids TS6133 + }); + + it('RotateIdentifierArgs', () => { + let args: RotateIdentifierArgs; + args = { + nsith: 1, + }; + args = { + nsith: '1', + }; + args = { + nsith: ['1'], + }; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + args !== null; // avoids TS6133 + }); + }); +}); diff --git a/packages/signify-ts/test/app/clienting.test.ts b/packages/signify-ts/test/app/clienting.test.ts new file mode 100644 index 00000000..b969ba7b --- /dev/null +++ b/packages/signify-ts/test/app/clienting.test.ts @@ -0,0 +1,312 @@ +import { assert, describe, it, test, expect } from 'vitest'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import { Identifier } from '../../src/keri/app/aiding.ts'; +import { + Operations, + KeyEvents, + KeyStates, + Oobis, +} from '../../src/keri/app/coring.ts'; +import { Contacts, Challenges } from '../../src/keri/app/contacting.ts'; +import { + Credentials, + Schemas, + Registries, +} from '../../src/keri/app/credentialing.ts'; +import { Escrows } from '../../src/keri/app/escrowing.ts'; +import { Exchanges } from '../../src/keri/app/exchanging.ts'; +import { Groups } from '../../src/keri/app/grouping.ts'; +import { Notifications } from '../../src/keri/app/notifying.ts'; + +import { + HEADER_SIG_INPUT, + HEADER_SIG_TIME, +} from '../../src/keri/core/httping.ts'; +import { Tier } from '../../src/keri/core/salter.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { createMockFetch } from './test-utils.ts'; + +const fetchMock = createMockFetch(); + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; +const bran = '0123456789abcdefghijk'; + +describe('SignifyClient', () => { + it('SignifyClient initialization', async () => { + await libsodium.ready; + + const t = () => { + new SignifyClient(url, 'short', Tier.low, boot_url); + }; + expect(t).toThrow('bran must be 21 characters'); + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + assert.equal(client.bran, '0123456789abcdefghijk'); + assert.equal(client.url, url); + assert.equal(client.bootUrl, boot_url); + assert.equal(client.tier, Tier.low); + assert.equal(client.pidx, 0); + assert.equal( + client.controller.pre, + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + assert.equal(client.controller.stem, 'signify:controller'); + assert.equal(client.controller.tier, Tier.low); + assert.equal( + client.controller.serder.raw, + '{"v":"KERI10JSON00012b_","t":"icp",' + + '"d":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose",' + + '"i":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","s":"0",' + + '"kt":"1","k":["DAbWjobbaLqRB94KiAutAHb_qzPpOHm3LURA_ksxetVc"],' + + '"nt":"1","n":["EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6xZZT_E3h7d2tL"],' + + '"bt":"0","b":[],"c":[],"a":[]}' + ); + assert.deepEqual(client.controller.serder.sad.s, '0'); + + const res = await client.boot(); + assert.equal(fetchMock.mock.calls[0]![0]!, boot_url + '/boot'); + assert.equal( + fetchMock.mock.calls[0]![1]!.body!.toString(), + '{"icp":{"v":"KERI10JSON00012b_","t":"icp","d":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","i":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","s":"0","kt":"1","k":["DAbWjobbaLqRB94KiAutAHb_qzPpOHm3LURA_ksxetVc"],"nt":"1","n":["EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6xZZT_E3h7d2tL"],"bt":"0","b":[],"c":[],"a":[]},"sig":"AACJwsJ0mvb4VgxD87H4jIsiT1QtlzznUy9zrX3lGdd48jjQRTv8FxlJ8ClDsGtkvK4Eekg5p-oPYiPvK_1eTXEG","stem":"signify:controller","pidx":1,"tier":"low"}' + ); + assert.equal(res.status, 202); + + await client.connect(); + + // validate agent + assert( + client.agent!.pre, + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + assert( + client.agent!.anchor, + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + assert( + client.agent!.said, + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + assert(client.agent!.state.s, '0'); + assert( + client.agent!.state.d, + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + + // validate approve delegation + assert.equal(client.controller.serder.sad.s, '1'); + assert.equal(client.controller.serder.sad.t, 'ixn'); + assert.equal( + client.controller.serder.sad.a[0].i, + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + assert.equal( + client.controller.serder.sad.a[0].d, + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + assert.equal(client.controller.serder.sad.a[0].s, '0'); + + const data = client.data; + assert(data[0], url); + assert(data[0], bran); + + assert.equal(client.identifiers() instanceof Identifier, true); + assert.equal(client.operations() instanceof Operations, true); + assert.equal(client.keyEvents() instanceof KeyEvents, true); + assert.equal(client.keyStates() instanceof KeyStates, true); + assert.equal(client.keyStates() instanceof KeyStates, true); + assert.equal(client.credentials() instanceof Credentials, true); + assert.equal(client.registries() instanceof Registries, true); + assert.equal(client.schemas() instanceof Schemas, true); + assert.equal(client.challenges() instanceof Challenges, true); + assert.equal(client.contacts() instanceof Contacts, true); + assert.equal(client.notifications() instanceof Notifications, true); + assert.equal(client.escrows() instanceof Escrows, true); + assert.equal(client.oobis() instanceof Oobis, true); + assert.equal(client.exchanges() instanceof Exchanges, true); + assert.equal(client.groups() instanceof Groups, true); + }); + + it('Signed fetch', async () => { + await libsodium.ready; + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.connect(); + + let resp = await client.fetch('/contacts', 'GET', undefined); + assert.equal(resp.status, 202); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/contacts'); + assert.equal(lastCall[1]!.method, 'GET'); + let lastHeaders = new Headers(lastCall[1]!.headers!); + assert.equal( + lastHeaders.get('signify-resource'), + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + + // Headers in error + let badAgentHeaders = { + 'signify-resource': 'bad_resource', + [HEADER_SIG_TIME]: '2023-08-20T15:34:31.534673+00:00', + [HEADER_SIG_INPUT]: + 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1692545671;keyid="EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei";alg="ed25519"', + signature: + 'indexed="?0";signify="0BDiSoxCv42h2BtGMHy_tpWAqyCgEoFwRa8bQy20mBB2D5Vik4gRp3XwkEHtqy6iy6SUYAytMUDtRbewotAfkCgN"', + 'content-type': 'application/json', + }; + fetchMock.mockResolvedValueOnce( + Response.json([], { + status: 202, + headers: badAgentHeaders, + }) + ); + let t = async () => await client.fetch('/contacts', 'GET', undefined); + await expect(t).rejects.toThrowError( + 'message from a different remote agent' + ); + + badAgentHeaders = { + 'signify-resource': 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'signify-timestamp': '2023-08-20T15:34:31.534673+00:00', + [HEADER_SIG_INPUT]: + 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1692545671;keyid="EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei";alg="ed25519"', + signature: + 'indexed="?0";signify="0BDiSoxCv42h2BtGMHy_tpWAqyCgEoFwRa8bQy20mBB2D5Vik4gRp3XwkEHtqy6iy6SUYAytMUDtRbewotAfkCbad"', + 'content-type': 'application/json', + }; + fetchMock.mockResolvedValueOnce( + Response.json([], { + status: 202, + headers: badAgentHeaders, + }) + ); + t = async () => await client.fetch('/contacts', 'GET', undefined); + await expect(t).rejects.toThrowError( + 'Signature for EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei invalid.' + ); + + // Other calls + resp = await client.saveOldPasscode('1234'); + assert.equal(resp.status, 202); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/salt/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + assert.equal(lastCall[1]!.method, 'PUT'); + assert.equal(lastCall[1]!.body, '{"salt":"1234"}'); + + resp = await client.deletePasscode(); + assert.equal(resp.status, 202); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/salt/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + assert.equal(lastCall[1]!.method, 'DELETE'); + + resp = await client.rotate('abcdefghijk0123456789', []); + assert.equal(resp.status, 202); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/agent/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + assert.equal(lastCall[1]!.method, 'PUT'); + let lastBody = JSON.parse(lastCall[1]!.body! as string); + assert.equal(lastBody.rot.t, 'rot'); + assert.equal(lastBody.rot.s, '1'); + assert.deepEqual(lastBody.rot.kt, ['1', '0']); + assert.equal( + lastBody.rot.d, + 'EGFi9pCcRaLK8dPh5S7JP9Em62fBMiR1l4gW1ZazuuAO' + ); + + const heads = new Headers(); + heads.set('Content-Type', 'application/json'); + const treqInit = { + headers: heads, + method: 'POST', + body: JSON.stringify({ foo: true }), + }; + const turl = 'http://example.com/test'; + const treq = await client.createSignedRequest('aid1', turl, treqInit); + assert.equal(treq.url, 'http://example.com/test'); + assert.equal(treq.method, 'POST'); + lastBody = await treq.json(); + assert.deepEqual(lastBody.foo, true); + lastHeaders = new Headers(treq.headers); + assert.equal( + lastHeaders.get('signify-resource'), + 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' + ); + assert.equal( + lastHeaders + .get(HEADER_SIG_INPUT) + ?.startsWith( + 'signify=("@method" "@path" "signify-resource" "signify-timestamp");created=' + ), + true + ); + assert.equal( + lastHeaders + .get(HEADER_SIG_INPUT) + ?.endsWith( + ';keyid="DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9";alg="ed25519"' + ), + true + ); + + const aid = await client.identifiers().get('aid1'); + const keeper = client.manager!.get(aid); + const signer = keeper.signers[0]; + const created = lastHeaders + .get(HEADER_SIG_INPUT) + ?.split(';created=')[1] + .split(';keyid=')[0]; + const data = `"@method": POST\n"@path": /test\n"signify-resource": ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK\n"signify-timestamp": ${lastHeaders.get( + HEADER_SIG_TIME + )}\n"@signature-params: (@method @path signify-resource signify-timestamp);created=${created};keyid=DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9;alg=ed25519"`; + + if (data) { + const raw = new TextEncoder().encode(data); + const sig = signer.sign(raw); + assert.equal( + sig.qb64, + lastHeaders + .get('signature') + ?.split('signify="')[1] + .split('"')[0] + ); + } else { + assert.fail(`${HEADER_SIG_INPUT} is empty`); + } + }); + + test('includes HTTP status info in error message', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.connect(); + + fetchMock.mockResolvedValue( + new Response('Error info', { + status: 400, + statusText: 'Bad Request', + }) + ); + + const error = await client + .fetch('/somepath', 'GET', undefined) + .catch((e) => e); + + assert(error instanceof Error); + assert.equal( + error.message, + 'HTTP GET /somepath - 400 Bad Request - Error info' + ); + }); +}); diff --git a/packages/signify-ts/test/app/contacting.test.ts b/packages/signify-ts/test/app/contacting.test.ts new file mode 100644 index 00000000..c4ff45ee --- /dev/null +++ b/packages/signify-ts/test/app/contacting.test.ts @@ -0,0 +1,160 @@ +import { assert, describe, it } from 'vitest'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import { Tier } from '../../src/keri/core/salter.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { createMockFetch } from './test-utils.ts'; + +const fetchMock = createMockFetch(); + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; + +describe('Contacting', () => { + it('Contacts', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const contacts = client.contacts(); + + await contacts.list('mygroup', 'company', 'mycompany'); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + + '/contacts?group=mygroup&filter_field=company&filter_value=mycompany' + ); + assert.equal(lastCall[1]!.method, 'GET'); + + await contacts.get('EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + assert.equal(lastCall[1]!.method, 'GET'); + + const info = { + name: 'John Doe', + company: 'My Company', + }; + await contacts.add( + 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + info + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + let lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal( + lastCall[0]!, + url + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + assert.equal(lastCall[1]!.method, 'POST'); + assert.deepEqual(lastBody, info); + + await contacts.update( + 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + info + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal( + lastCall[0]!, + url + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + assert.equal(lastCall[1]!.method, 'PUT'); + assert.deepEqual(lastBody, info); + + await contacts.delete('EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + assert.equal(lastCall[1]!.method, 'DELETE'); + assert.equal(lastCall[1]!.body, undefined); + }); + + it('Challenges', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const challenges = client.challenges(); + + await challenges.generate(128); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/challenges?strength=128'); + assert.equal(lastCall[1]!.method, 'GET'); + + const words = [ + 'shell', + 'gloom', + 'mimic', + 'cereal', + 'stool', + 'furnace', + 'nominee', + 'nation', + 'sauce', + 'sausage', + 'rather', + 'venue', + ]; + await challenges.respond( + 'aid1', + 'EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p', + words + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/identifiers/aid1/exchanges'); + assert.equal(lastCall[1]!.method, 'POST'); + let lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastBody.tpc, 'challenge'); + assert.equal(lastBody.exn.r, '/challenge/response'); + assert.equal( + lastBody.exn.i, + 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' + ); + assert.deepEqual(lastBody.exn.a.words, words); + assert.equal(lastBody.sigs[0].length, 88); + + await challenges.verify( + 'EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p', + words + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + + '/challenges_verify/EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p' + ); + assert.equal(lastCall[1]!.method, 'POST'); + lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.deepEqual(lastBody.words, words); + + await challenges.responded( + 'EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p', + 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + + '/challenges_verify/EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p' + ); + assert.equal(lastCall[1]!.method, 'PUT'); + lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal( + lastBody.said, + 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + }); +}); diff --git a/packages/signify-ts/test/app/controller.test.ts b/packages/signify-ts/test/app/controller.test.ts new file mode 100644 index 00000000..a4c814a3 --- /dev/null +++ b/packages/signify-ts/test/app/controller.test.ts @@ -0,0 +1,58 @@ +import { Controller } from '../../src/keri/app/controller.ts'; +import { assert, describe, it } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; +import { openManager } from '../../src/keri/core/manager.ts'; +import { Signer } from '../../src/keri/core/signer.ts'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import { Tier, randomPasscode } from '../../src/index.ts'; + +describe('Controller', () => { + it('manage account AID signing and agent verification', async () => { + await libsodium.ready; + let passcode = '0123456789abcdefghijk'; + const mgr = openManager(passcode); + assert.equal(mgr.aeid, 'BMbZTXzB7LmWPT2TXLGV88PQz5vDEM2L2flUs2yxn3U9'); + + const raw = new Uint8Array([ + 187, 140, 234, 145, 219, 254, 20, 194, 16, 18, 97, 194, 140, 192, + 61, 145, 222, 110, 59, 160, 152, 2, 72, 122, 87, 143, 109, 39, 98, + 153, 192, 148, + ]); + const agentSigner = new Signer({ + raw: raw, + code: MtrDex.Ed25519_Seed, + transferable: false, + }); + assert.equal( + agentSigner.verfer.qb64, + 'BHptu91ecGv_mxO8T3b98vNQUCghT8nfYkWRkVqOZark' + ); + + // New account needed. Send to remote my name and encryption pubk and get back + // their pubk and and my encrypted account package + // let pkg = {} + let controller = new Controller(passcode, Tier.low); + assert.equal( + controller.pre, + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + + passcode = 'abcdefghijk0123456789'; + controller = new Controller(passcode, Tier.low); + assert.equal( + controller.pre, + 'EIIY2SgE_bqKLl2MlnREUawJ79jTuucvWwh-S6zsSUFo' + ); + }); + + it('should generate unique controller AIDs per passcode', async () => { + await libsodium.ready; + const passcode1 = randomPasscode(); + const passcode2 = randomPasscode(); + + const controller1 = new Controller(passcode1, Tier.low); + const controller2 = new Controller(passcode2, Tier.low); + + assert.notEqual(controller1.pre, controller2.pre); + }); +}); diff --git a/packages/signify-ts/test/app/coring.test.ts b/packages/signify-ts/test/app/coring.test.ts new file mode 100644 index 00000000..53be7043 --- /dev/null +++ b/packages/signify-ts/test/app/coring.test.ts @@ -0,0 +1,331 @@ +import { assert, describe, it, beforeEach, vitest } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; +import { + randomPasscode, + randomNonce, + Operations, + OperationsDeps, +} from '../../src/keri/app/coring.ts'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import { Tier } from '../../src/keri/core/salter.ts'; +import { randomUUID } from 'node:crypto'; +import { createMockFetch } from './test-utils.ts'; + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; +const fetchMock = createMockFetch(); + +describe('Coring', () => { + it('Random passcode', async () => { + await libsodium.ready; + const passcode = randomPasscode(); + assert.equal(passcode.length, 21); + }); + + it('Random nonce', async () => { + await libsodium.ready; + const nonce = randomNonce(); + assert.equal(nonce.length, 44); + }); + + it('OOBIs', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const oobis = client.oobis(); + + await oobis.get('aid', 'agent'); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/identifiers/aid/oobis?role=agent'); + assert.equal(lastCall[1]!.method, 'GET'); + + await oobis.resolve('http://oobiurl.com'); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + let lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastCall[0]!, url + '/oobis'); + assert.equal(lastCall[1]!.method, 'POST'); + assert.deepEqual(lastBody.url, 'http://oobiurl.com'); + + await oobis.resolve('http://oobiurl.com', 'witness'); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastCall[0]!, url + '/oobis'); + assert.equal(lastCall[1]!.method, 'POST'); + assert.deepEqual(lastBody.url, 'http://oobiurl.com'); + assert.deepEqual(lastBody.oobialias, 'witness'); + }); + + it('Events and states', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const keyEvents = client.keyEvents(); + const keyStates = client.keyStates(); + + await keyEvents.get('EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/events?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' + ); + assert.equal(lastCall[1]!.method, 'GET'); + + await keyStates.get('EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/states?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' + ); + assert.equal(lastCall[1]!.method, 'GET'); + + await keyStates.list([ + 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX', + 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + ]); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + + '/states?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX&pre=ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' + ); + assert.equal(lastCall[1]!.method, 'GET'); + + await keyStates.query( + 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX', + '1', + 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + const lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastCall[0]!, url + '/queries'); + assert.equal(lastCall[1]!.method, 'POST'); + assert.equal( + lastBody.pre, + 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' + ); + assert.equal(lastBody.sn, '1'); + assert.equal( + lastBody.anchor, + 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + }); + + it('Agent configuration', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const config = client.config(); + + await config.get(); + const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/config'); + assert.equal(lastCall[1]!.method, 'GET'); + }); +}); + +describe('Operations', () => { + class MockClient implements OperationsDeps { + fetch = vitest.fn(); + + constructor() {} + + operations() { + return new Operations(this); + } + + getLastMockRequest() { + const [pathname, method, body] = this.fetch.mock.lastCall ?? []; + + return { + path: pathname, + method: method, + body: body, + }; + } + } + + let client: MockClient; + beforeEach(async () => { + await libsodium.ready; + client = new MockClient(); + }); + + it('Can get operation by name', async () => { + await libsodium.ready; + + client.fetch.mockResolvedValue( + new Response(JSON.stringify({ name: randomUUID() }), { + status: 200, + }) + ); + await client.operations().get('operationName'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/operations/operationName'); + assert.equal(lastCall.method, 'GET'); + }); + + it('Can list operations', async () => { + client.fetch.mockResolvedValue( + new Response(JSON.stringify([]), { + status: 200, + }) + ); + await client.operations().list(); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/operations?'); + assert.equal(lastCall.method, 'GET'); + }); + + it('Can list operations by type', async () => { + client.fetch.mockResolvedValue( + new Response(JSON.stringify([]), { + status: 200, + }) + ); + await client.operations().list('witness'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/operations?type=witness'); + assert.equal(lastCall.method, 'GET'); + }); + + it('Can delete operation by name', async () => { + client.fetch.mockResolvedValue( + new Response(JSON.stringify({}), { + status: 200, + }) + ); + await client.operations().delete('operationName'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/operations/operationName'); + assert.equal(lastCall.method, 'DELETE'); + }); + + describe('wait', () => { + it('does not wait for operation that is already "done"', async () => { + const name = randomUUID(); + client.fetch.mockResolvedValue( + new Response(JSON.stringify({ name }), { + status: 200, + }) + ); + + const op = { name, done: true }; + const result = await client.operations().wait(op); + assert.equal(client.fetch.mock.calls.length, 0); + assert.equal(op, result); + }); + + it('returns when operation is done after first call', async () => { + const name = randomUUID(); + client.fetch.mockResolvedValue( + new Response(JSON.stringify({ name, done: true }), { + status: 200, + }) + ); + + const op = { name, done: false }; + await client.operations().wait(op); + assert.equal(client.fetch.mock.calls.length, 1); + }); + + it('returns when operation is done after second call', async () => { + const name = randomUUID(); + client.fetch.mockResolvedValueOnce( + new Response(JSON.stringify({ name, done: false }), { + status: 200, + }) + ); + + client.fetch.mockResolvedValueOnce( + new Response(JSON.stringify({ name, done: true }), { + status: 200, + }) + ); + + const op = { name, done: false }; + await client.operations().wait(op, { maxSleep: 10 }); + assert.equal(client.fetch.mock.calls.length, 2); + }); + + it('throw if aborted', async () => { + const name = randomUUID(); + client.fetch.mockImplementation( + async () => + new Response(JSON.stringify({ name, done: false }), { + status: 200, + }) + ); + + const op = { name, done: false }; + + const controller = new AbortController(); + const promise = client + .operations() + .wait(op, { signal: controller.signal }) + .catch((e) => e); + + const abortError = new Error('Aborted'); + controller.abort(abortError); + + const error = await promise; + + assert.equal(error, abortError); + }); + + it('returns when child operation is also done', async () => { + const name = randomUUID(); + const nestedName = randomUUID(); + const depends = { name: nestedName, done: false }; + const op = { name, done: false, depends }; + + client.fetch.mockResolvedValueOnce( + new Response(JSON.stringify({ ...op, done: false }), { + status: 200, + }) + ); + + client.fetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + ...op, + depends: { ...depends, done: true }, + }), + { + status: 200, + } + ) + ); + + client.fetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + ...op, + done: true, + depends: { ...depends, done: true }, + }), + { + status: 200, + } + ) + ); + + await client.operations().wait(op, { maxSleep: 10 }); + assert.equal(client.fetch.mock.calls.length, 3); + }); + }); +}); diff --git a/packages/signify-ts/test/app/credentialing.test.ts b/packages/signify-ts/test/app/credentialing.test.ts new file mode 100644 index 00000000..e298fba2 --- /dev/null +++ b/packages/signify-ts/test/app/credentialing.test.ts @@ -0,0 +1,679 @@ +import { assert, describe, it } from 'vitest'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; + +import { Tier } from '../../src/keri/core/salter.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { + d, + Protocols, + Ilks, + interact, + Saider, + Serder, + serializeACDCAttachment, + serializeIssExnAttachment, + Serials, + versify, +} from '../../src/index.ts'; +import { createMockFetch, mockCredential } from './test-utils.ts'; + +const fetchMock = createMockFetch(); + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; + +describe('Credentialing', () => { + it('Credentials', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const credentials = client.credentials(); + + const kargs = { + filter: { + '-i': { $eq: 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' }, + }, + sort: [{ '-s': 1 }], + limit: 25, + skip: 5, + }; + await credentials.list(kargs); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + let lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastCall[0]!, url + '/credentials/query'); + assert.equal(lastCall[1]!.method, 'POST'); + assert.deepEqual(lastBody, kargs); + + await credentials.get( + 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + true + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/credentials/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + assert.equal(lastCall[1]!.method, 'GET'); + + const registry = 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'; + const schema = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; + const isuee = 'EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p'; + await credentials.issue('aid1', { + ri: registry, + s: schema, + a: { i: isuee, LEI: '1234' }, + }); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastCall[0]!, url + '/identifiers/aid1/credentials'); + assert.equal(lastCall[1]!.method, 'POST'); + assert.equal(lastBody.acdc.ri, registry); + assert.equal(lastBody.acdc.s, schema); + assert.equal(lastBody.acdc.a.i, isuee); + assert.equal(lastBody.acdc.a.LEI, '1234'); + assert.equal(lastBody.iss.s, '0'); + assert.equal(lastBody.iss.t, 'iss'); + assert.equal(lastBody.iss.ri, registry); + assert.equal(lastBody.iss.i, lastBody.acdc.d); + assert.equal(lastBody.ixn.t, 'ixn'); + assert.equal(lastBody.ixn.i, lastBody.acdc.i); + assert.equal(lastBody.ixn.p, lastBody.acdc.i); + assert.equal(lastBody.sigs[0].substring(0, 2), 'AA'); + assert.equal(lastBody.sigs[0].length, 88); + + const credential = lastBody.acdc.i; + await credentials.revoke('aid1', credential); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal( + lastCall[0]!, + url + '/identifiers/aid1/credentials/' + credential + ); + assert.equal(lastCall[1]!.method, 'DELETE'); + assert.equal(lastBody.rev.s, '1'); + assert.equal(lastBody.rev.t, 'rev'); + assert.equal( + lastBody.rev.ri, + 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df' + ); + assert.equal( + lastBody.rev.i, + 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' + ); + assert.equal(lastBody.ixn.t, 'ixn'); + assert.equal( + lastBody.ixn.i, + 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' + ); + assert.equal( + lastBody.ixn.p, + 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' + ); + assert.equal(lastBody.sigs[0].substring(0, 2), 'AA'); + assert.equal(lastBody.sigs[0].length, 88); + + await credentials.state(mockCredential.sad.ri, mockCredential.sad.d); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + + '/registries/EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df/EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo' + ); + assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1]!.body, null); + + await credentials.delete(mockCredential.sad.d); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/credentials/EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo' + ); + assert.equal(lastCall[1]!.method, 'DELETE'); + assert.equal(lastCall[1]!.body, undefined); + }); +}); + +describe('Ipex', () => { + it('IPEX - grant-admit flow initiated by discloser', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const ipex = client.ipex(); + + const holder = 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k'; + const [, acdc] = Saider.saidify(mockCredential.sad); + + // Create iss + const vs = versify(Protocols.KERI, undefined, Serials.JSON, 0); + const _iss = { + v: vs, + t: Ilks.iss, + d: '', + i: mockCredential.sad.d, + s: '0', + ri: mockCredential.sad.ri, + dt: mockCredential.sad.a.dt, + }; + + const [, iss] = Saider.saidify(_iss); + const iserder = new Serder(iss); + const anc = interact({ + pre: mockCredential.sad.i, + sn: 1, + data: [{}], + dig: mockCredential.sad.d, + version: undefined, + kind: undefined, + }); + + const [grant, gsigs, end] = await ipex.grant({ + senderName: 'multisig', + recipient: holder, + message: '', + acdc: new Serder(acdc), + iss: iserder, + anc, + datetime: mockCredential.sad.a.dt, + }); + + assert.deepStrictEqual(grant.sad, { + v: 'KERI10JSON0004e5_', + t: 'exn', + d: 'EPVuNFwXTG56BvNtGjeyxncY-MfZMXOAgEtsmIvktkdb', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + p: '', + dt: '2023-08-23T15:16:07.553000+00:00', + r: '/ipex/grant', + rp: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + q: {}, + a: { m: '', i: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k' }, + e: { + acdc: { + v: 'ACDC10JSON000197_', + d: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + i: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + ri: 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df', + s: 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + a: { + d: 'EK0GOjijKd8_RLYz9qDuuG29YbbXjU8yJuTQanf07b6P', + i: 'EKvn1M6shPLnXTb47bugVJblKMuWC0TcLIePP8p98Bby', + dt: '2023-08-23T15:16:07.553000+00:00', + LEI: '5493001KJTIIGC8Y1R17', + }, + }, + iss: { + v: 'KERI10JSON0000ed_', + t: 'iss', + d: 'ENf3IEYwYtFmlq5ZzoI-zFzeR7E3ZNRN2YH_0KAFbdJW', + i: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + s: '0', + ri: 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df', + dt: '2023-08-23T15:16:07.553000+00:00', + }, + anc: { + v: 'KERI10JSON0000cd_', + t: 'ixn', + d: 'ECVCyxNpB4PJkpLbWqI02WXs1wf7VUxPNY2W28SN2qqm', + i: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + s: '1', + p: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + a: [{}], + }, + d: 'EGpSjqjavdzgjQiyt0AtrOutWfKrj5gR63lOUUq-1sL-', + }, + }); + + assert.deepStrictEqual(gsigs, [ + 'AADGVl57V4gcKYPO_Dn4UuYIdHI62vEQP--U3pnsl8oCqiqQbRqjw2E_7PHBy5-U78de5rhfF4UZQBFeub5evO8M', + ]); + assert.equal( + end, + '-LAg4AACA-e-acdc-IABEMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo0AAAAAAAAAAAAAAAAAAAAAAAENf3IEYwYtFmlq5Zz' + + 'oI-zFzeR7E3ZNRN2YH_0KAFbdJW-LAW5AACAA-e-iss-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAAAECVCyxNpB4PJkpLbWqI02WXs1wf7VU' + + 'xPNY2W28SN2qqm-LAa5AACAA-e-anc-AABAADMtDfNihvCSXJNp1VronVojcPGo--0YZ4Kh6CAnowRnn4Or4FgZQqaqCEv6XVS413qfZo' + + 'Vp8j2uxTTPkItO7ED' + ); + + const [ng, ngsigs, ngend] = await ipex.grant({ + senderName: 'multisig', + recipient: holder, + message: '', + acdc: new Serder(acdc), + acdcAttachment: d(serializeACDCAttachment(iserder)), + iss: iserder, + issAttachment: d(serializeIssExnAttachment(anc)), + anc, + ancAttachment: + '-AABAADMtDfNihvCSXJNp1VronVojcPGo--0YZ4Kh6CAnowRnn4Or4FgZQqaqCEv6XVS413qfZoVp8j2uxTTPkItO7ED', + datetime: mockCredential.sad.a.dt, + }); + + assert.deepStrictEqual(ng.sad, grant.sad); + assert.deepStrictEqual(ngsigs, gsigs); + assert.deepStrictEqual(ngend, ngend); + + await ipex.submitGrant('multisig', ng, ngsigs, ngend, [holder]); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0], + 'http://127.0.0.1:3901/identifiers/multisig/ipex/grant' + ); + + const [admit, asigs, aend] = await ipex.admit({ + senderName: 'holder', + message: '', + grantSaid: grant.sad.d, + recipient: holder, + datetime: mockCredential.sad.a.dt, + }); + + assert.deepStrictEqual(admit.sad, { + v: 'KERI10JSON000178_', + t: 'exn', + d: 'EJrfQsTZhkHC6vDEwkbWISpbBk9HFLO3NuI5uByYw8tH', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + p: 'EPVuNFwXTG56BvNtGjeyxncY-MfZMXOAgEtsmIvktkdb', + dt: '2023-08-23T15:16:07.553000+00:00', + r: '/ipex/admit', + rp: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + q: {}, + a: { m: '', i: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k' }, + e: {}, + }); + + assert.deepStrictEqual(asigs, [ + 'AAC4MTRQR-U8_3Hf53f2nJuh3n93lauXSHUkF1Yk2diTHwF-qkcBHn_jd-6pgRnRtBV2CInfwZyOsSL2CrRyuNEN', + ]); + + await ipex.submitAdmit('multisig', admit, asigs, aend, [holder]); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0], + 'http://127.0.0.1:3901/identifiers/multisig/ipex/admit' + ); + + assert.equal(aend, ''); + }); + + it( + 'IPEX - apply-admit flow initiated by disclosee', + { timeout: 10000 }, + async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const ipex = client.ipex(); + + const holder = 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k'; + const [, acdc] = Saider.saidify(mockCredential.sad); + + // Create iss + const vs = versify(Protocols.KERI, undefined, Serials.JSON, 0); + const _iss = { + v: vs, + t: Ilks.iss, + d: '', + i: mockCredential.sad.d, + s: '0', + ri: mockCredential.sad.ri, + dt: mockCredential.sad.a.dt, + }; + + const [, iss] = Saider.saidify(_iss); + const iserder = new Serder(iss); + const anc = interact({ + pre: mockCredential.sad.i, + sn: 1, + data: [{}], + dig: mockCredential.sad.d, + version: undefined, + kind: undefined, + }); + + const [apply, applySigs, applyEnd] = await ipex.apply({ + senderName: 'multisig', + recipient: holder, + message: 'Applying', + schemaSaid: mockCredential.sad.s, + attributes: { LEI: mockCredential.sad.a.LEI }, + datetime: mockCredential.sad.a.dt, + }); + + assert.deepStrictEqual(apply.sad, { + v: 'KERI10JSON0001aa_', + t: 'exn', + d: 'ELjIE5cr_M2r7oUYw2pwcdNY_ZBuEgRlefaP0zSs_bXL', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + p: '', + dt: '2023-08-23T15:16:07.553000+00:00', + r: '/ipex/apply', + rp: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + q: {}, + a: { + m: 'Applying', + i: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + s: 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + a: { LEI: '5493001KJTIIGC8Y1R17' }, + }, + e: {}, + }); + + assert.deepStrictEqual(applySigs, [ + 'AADJYSkOTxd8KfH4YUKWWjkNynAH4fm3wcKOPmepLiI_iuNPV9TL-sIRxLeCBG5rQmqXtnSP0Wi6jgI7sHC9PBgF', + ]); + + assert.equal(applyEnd, ''); + + await ipex.submitApply('multisig', apply, applySigs, [holder]); + let lastCall = + fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0], + 'http://127.0.0.1:3901/identifiers/multisig/ipex/apply' + ); + + const [offer, offerSigs, offerEnd] = await ipex.offer({ + senderName: 'multisig', + recipient: holder, + message: 'How about this', + acdc: new Serder(acdc), + datetime: mockCredential.sad.a.dt, + applySaid: apply.sad.d, + }); + + assert.deepStrictEqual(offer.sad, { + v: 'KERI10JSON000357_', + t: 'exn', + d: 'EBkyi_fhfnDWJXi4FW6t_o4F7Oep3PvSZ6E-qT716kfU', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + p: 'ELjIE5cr_M2r7oUYw2pwcdNY_ZBuEgRlefaP0zSs_bXL', + dt: '2023-08-23T15:16:07.553000+00:00', + r: '/ipex/offer', + rp: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + q: {}, + a: { + m: 'How about this', + i: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + }, + e: { + acdc: { + v: 'ACDC10JSON000197_', + d: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + i: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + ri: 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df', + s: 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + a: { + d: 'EK0GOjijKd8_RLYz9qDuuG29YbbXjU8yJuTQanf07b6P', + i: 'EKvn1M6shPLnXTb47bugVJblKMuWC0TcLIePP8p98Bby', + dt: '2023-08-23T15:16:07.553000+00:00', + LEI: '5493001KJTIIGC8Y1R17', + }, + }, + d: 'EK72JZyOyz81Jvt--iebptfhIWiw2ZdQg7ondKd-EyJF', + }, + }); + + assert.deepStrictEqual(offerSigs, [ + 'AADUeKpUxTKVS1DYRuHC3YDM8T4YMREnQLi00QiJH2Q_WjtMZTd7rBLH12xAJkt8h4KEOn4U_c-jpHdj9S9qKXsO', + ]); + assert.equal(offerEnd, ''); + + await ipex.submitOffer('multisig', offer, offerSigs, offerEnd, [ + holder, + ]); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0], + 'http://127.0.0.1:3901/identifiers/multisig/ipex/offer' + ); + + const [agree, agreeSigs, agreeEnd] = await ipex.agree({ + senderName: 'multisig', + recipient: holder, + message: 'OK!', + datetime: mockCredential.sad.a.dt, + offerSaid: offer.sad.d, + }); + + assert.deepStrictEqual(agree.sad, { + v: 'KERI10JSON00017b_', + t: 'exn', + d: 'EDLk56nlLrPHzhy3-5BHkhBNi-7tWUseWL_83I5QRmZ8', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + p: 'EBkyi_fhfnDWJXi4FW6t_o4F7Oep3PvSZ6E-qT716kfU', + dt: '2023-08-23T15:16:07.553000+00:00', + r: '/ipex/agree', + rp: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + q: {}, + a: { + m: 'OK!', + i: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + }, + e: {}, + }); + + assert.deepStrictEqual(agreeSigs, [ + 'AADgFlQVwRU7PF_gi4_o-wEgh3lZxzDtiwnIr9XFBrLOxhR6nBJNhrHZ_MkagCQcFHMpFkD9Vhxgq8HkV2gssPcO', + ]); + assert.equal(agreeEnd, ''); + + await ipex.submitAgree('multisig', agree, agreeSigs, [holder]); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0], + 'http://127.0.0.1:3901/identifiers/multisig/ipex/agree' + ); + + const [grant, gsigs, end] = await ipex.grant({ + senderName: 'multisig', + recipient: holder, + message: '', + acdc: new Serder(acdc), + iss: iserder, + anc, + datetime: mockCredential.sad.a.dt, + agreeSaid: agree.sad.d, + }); + + assert.deepStrictEqual(grant.sad, { + v: 'KERI10JSON000511_', + t: 'exn', + d: 'ENwwMpAuZ3NaZqqeydm3G18EDZFWuHzeJMfzfwNkb99N', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + p: 'EDLk56nlLrPHzhy3-5BHkhBNi-7tWUseWL_83I5QRmZ8', + dt: '2023-08-23T15:16:07.553000+00:00', + r: '/ipex/grant', + rp: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + q: {}, + a: { m: '', i: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k' }, + e: { + acdc: { + v: 'ACDC10JSON000197_', + d: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + i: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + ri: 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df', + s: 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + a: { + d: 'EK0GOjijKd8_RLYz9qDuuG29YbbXjU8yJuTQanf07b6P', + i: 'EKvn1M6shPLnXTb47bugVJblKMuWC0TcLIePP8p98Bby', + dt: '2023-08-23T15:16:07.553000+00:00', + LEI: '5493001KJTIIGC8Y1R17', + }, + }, + iss: { + v: 'KERI10JSON0000ed_', + t: 'iss', + d: 'ENf3IEYwYtFmlq5ZzoI-zFzeR7E3ZNRN2YH_0KAFbdJW', + i: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + s: '0', + ri: 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df', + dt: '2023-08-23T15:16:07.553000+00:00', + }, + anc: { + v: 'KERI10JSON0000cd_', + t: 'ixn', + d: 'ECVCyxNpB4PJkpLbWqI02WXs1wf7VUxPNY2W28SN2qqm', + i: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + s: '1', + p: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + a: [{}], + }, + d: 'EGpSjqjavdzgjQiyt0AtrOutWfKrj5gR63lOUUq-1sL-', + }, + }); + + assert.deepStrictEqual(gsigs, [ + 'AAB61_g8jLGO1vx8Fadd6UrDItNACwFAiuAvWGrm_szxWWNZwT21V0N79Q7bRHNdVzZudgAKVUhNUHhnwrUW6jsK', + ]); + assert.equal( + end, + '-LAg4AACA-e-acdc-IABEMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo0AAAAAAAAAAAAAAAAAAAAAAAENf3IEYwYtFmlq5Zz' + + 'oI-zFzeR7E3ZNRN2YH_0KAFbdJW-LAW5AACAA-e-iss-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAAAECVCyxNpB4PJkpLbWqI02WXs1wf7VU' + + 'xPNY2W28SN2qqm-LAa5AACAA-e-anc-AABAADMtDfNihvCSXJNp1VronVojcPGo--0YZ4Kh6CAnowRnn4Or4FgZQqaqCEv6XVS413qfZo' + + 'Vp8j2uxTTPkItO7ED' + ); + + const [ng, ngsigs, ngend] = await ipex.grant({ + senderName: 'multisig', + recipient: holder, + message: '', + acdc: new Serder(acdc), + acdcAttachment: d(serializeACDCAttachment(iserder)), + iss: iserder, + issAttachment: d(serializeIssExnAttachment(anc)), + anc, + ancAttachment: + '-AABAADMtDfNihvCSXJNp1VronVojcPGo--0YZ4Kh6CAnowRnn4Or4FgZQqaqCEv6XVS413qfZoVp8j2uxTTPkItO7ED', + datetime: mockCredential.sad.a.dt, + agreeSaid: agree.sad.d, + }); + + assert.deepStrictEqual(ng.sad, grant.sad); + assert.deepStrictEqual(ngsigs, gsigs); + assert.deepStrictEqual(ngend, ngend); + + await ipex.submitGrant('multisig', ng, ngsigs, ngend, [holder]); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0], + 'http://127.0.0.1:3901/identifiers/multisig/ipex/grant' + ); + + const [admit, asigs, aend] = await ipex.admit({ + senderName: 'holder', + message: '', + recipient: holder, + grantSaid: grant.sad.d, + datetime: mockCredential.sad.a.dt, + }); + + assert.deepStrictEqual(admit.sad, { + v: 'KERI10JSON000178_', + t: 'exn', + d: 'EPcEK9tPuLOHbLiPm_FETkIVLjHhwuUiZDRDKW6Hh0JF', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + p: 'ENwwMpAuZ3NaZqqeydm3G18EDZFWuHzeJMfzfwNkb99N', + dt: '2023-08-23T15:16:07.553000+00:00', + r: '/ipex/admit', + rp: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + q: {}, + a: { m: '', i: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k' }, + e: {}, + }); + + assert.deepStrictEqual(asigs, [ + 'AABqIUE6czxB5BotjxFUZT9Gu8tkFkAx7bOYQzWD422r-HS8z_6gaNuIlpnABHjxlX7PEXFDTj8WnoGVW197XlQP', + ]); + + await ipex.submitAdmit('multisig', admit, asigs, aend, [holder]); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0], + 'http://127.0.0.1:3901/identifiers/multisig/ipex/admit' + ); + + assert.equal(aend, ''); + } + ); + + it('IPEX - discloser can create an offer without apply', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const ipex = client.ipex(); + + const holder = 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k'; + const [, acdc] = Saider.saidify(mockCredential.sad); + + const [offer, offerSigs, offerEnd] = await ipex.offer({ + senderName: 'multisig', + recipient: holder, + message: 'Offering this', + acdc: new Serder(acdc), + datetime: mockCredential.sad.a.dt, + }); + + assert.deepStrictEqual(offer.sad, { + v: 'KERI10JSON00032a_', + t: 'exn', + d: 'EFmPdhVnJIrMZ0b6Nyk-4s2NP1InR3wgvBGcbxl2Cd8i', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + p: '', + dt: '2023-08-23T15:16:07.553000+00:00', + r: '/ipex/offer', + rp: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + q: {}, + a: { + m: 'Offering this', + i: 'ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k', + }, + e: { + acdc: { + v: 'ACDC10JSON000197_', + d: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + i: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + ri: 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df', + s: 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + a: { + d: 'EK0GOjijKd8_RLYz9qDuuG29YbbXjU8yJuTQanf07b6P', + i: 'EKvn1M6shPLnXTb47bugVJblKMuWC0TcLIePP8p98Bby', + dt: '2023-08-23T15:16:07.553000+00:00', + LEI: '5493001KJTIIGC8Y1R17', + }, + }, + d: 'EK72JZyOyz81Jvt--iebptfhIWiw2ZdQg7ondKd-EyJF', + }, + }); + + assert.deepStrictEqual(offerSigs, [ + 'AACeQZ8RAcD2qFbkGXiUAQRJpZL4qanNH50a0LnkrflOC9JB2UJo3vvy3buiOSLoo0z9uMNhqa79ToXwVCAxg9MK', + ]); + assert.equal(offerEnd, ''); + + await ipex.submitOffer('multisig', offer, offerSigs, offerEnd, [ + holder, + ]); + const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0], + 'http://127.0.0.1:3901/identifiers/multisig/ipex/offer' + ); + }); +}); diff --git a/packages/signify-ts/test/app/delegating.test.ts b/packages/signify-ts/test/app/delegating.test.ts new file mode 100644 index 00000000..48bff125 --- /dev/null +++ b/packages/signify-ts/test/app/delegating.test.ts @@ -0,0 +1,60 @@ +import { assert, describe, it } from 'vitest'; +import { Tier } from '../../src/index.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import { createMockFetch } from './test-utils.ts'; + +const fetchMock = createMockFetch(); + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; + +describe('delegate', () => { + it('approve delegation', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + const client = new SignifyClient(url, bran, Tier.low, boot_url); + await client.boot(); + await client.connect(); + const delegations = client.delegations(); + await delegations.approve( + 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + + '/identifiers/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao/delegation' + ); + assert.equal(lastCall[1]!.method, 'POST'); + const expectedBody = { + ixn: { + v: 'KERI10JSON0000cf_', + t: 'ixn', + d: 'EBPt7hivibUQN-dlRyE9x_Y5LgFCGJ8QoNLSJrIkBYIg', + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: '1', + p: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + a: [null], + }, + sigs: [ + 'AAC4StAw-0IiV_LujceAXB3tnkaK011rPYPBKLgz-u6jI7hwfWGTCu5LDvBUsON4CqXbZAwPgIv6JqYjIusWKv0G', + ], + salty: { + sxlt: '1AAHnNQTkD0yxOC9tSz_ukbB2e-qhDTStH18uCsi5PCwOyXLONDR3MeKwWv_AVJKGKGi6xiBQH25_R1RXLS2OuK3TN3ovoUKH7-A', + pidx: 0, + kidx: 0, + stem: 'signify:aid', + tier: 'low', + icodes: ['A'], + ncodes: ['A'], + dcode: 'E', + transferable: true, + }, + }; + assert.equal( + lastCall[1]?.body?.toString(), + JSON.stringify(expectedBody) + ); + }); +}); diff --git a/packages/signify-ts/test/app/escrowing.test.ts b/packages/signify-ts/test/app/escrowing.test.ts new file mode 100644 index 00000000..9150e9cf --- /dev/null +++ b/packages/signify-ts/test/app/escrowing.test.ts @@ -0,0 +1,33 @@ +import { assert, describe, it } from 'vitest'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import { Tier } from '../../src/keri/core/salter.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { createMockFetch } from './test-utils.ts'; + +const fetchMock = createMockFetch(); + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; + +describe('SignifyClient', () => { + it('Escrows', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const escrows = client.escrows(); + + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + await escrows.listReply('/presentation/request'); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/escrows/rpy?route=%2Fpresentation%2Frequest' + ); + assert.equal(lastCall[1]!.method, 'GET'); + }); +}); diff --git a/packages/signify-ts/test/app/exchanging.test.ts b/packages/signify-ts/test/app/exchanging.test.ts new file mode 100644 index 00000000..9b79b53e --- /dev/null +++ b/packages/signify-ts/test/app/exchanging.test.ts @@ -0,0 +1,256 @@ +import { assert, describe, it } from 'vitest'; +import { + b, + d, + Diger, + exchange, + Ilks, + MtrDex, + Salter, + Serder, + Tier, +} from '../../src/index.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import { createMockFetch } from './test-utils.ts'; + +const fetchMock = createMockFetch(); + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; + +describe('exchange', () => { + it('should create an exchange message with no transposed attachments', async () => { + await libsodium.ready; + const dt = '2023-08-30T17:22:54.183Z'; + + let [exn, end] = exchange('/multisig/vcp', {}, 'test', '', dt); + assert.deepStrictEqual(exn.sad, { + a: { + i: '', + }, + d: 'EPWm8LWxxQXmXlB8gbTZKDy7NIwXxpx49N_ZYTa5QkJV', + dt: '2023-08-30T17:22:54.183Z', + e: {}, + i: 'test', + p: '', + q: {}, + r: '/multisig/vcp', + rp: '', + t: 'exn', + v: 'KERI10JSON0000bf_', + }); + assert.deepStrictEqual(end, new Uint8Array()); + + const sith = 1; + const nsith = 1; + const sn = 0; + const toad = 0; + + const raw = new Uint8Array([ + 5, 170, 143, 45, 83, 154, 233, 250, 85, 156, 2, 156, 155, 8, 72, + 117, + ]); + const salter = new Salter({ raw: raw }); + const skp0 = salter.signer( + MtrDex.Ed25519_Seed, + true, + 'A', + Tier.low, + true + ); + const keys = [skp0.verfer.qb64]; + + const skp1 = salter.signer( + MtrDex.Ed25519_Seed, + true, + 'N', + Tier.low, + true + ); + const ndiger = new Diger({}, skp1.verfer.qb64b); + const nxt = [ndiger.qb64]; + assert.deepStrictEqual(nxt, [ + 'EAKUR-LmLHWMwXTLWQ1QjxHrihBmwwrV2tYaSG7hOrWj', + ]); + + const ked0 = { + v: 'KERI10JSON000000_', + t: Ilks.icp, + d: '', + i: '', + s: sn.toString(16), + kt: sith.toString(16), + k: keys, + nt: nsith.toString(16), + n: nxt, + bt: toad.toString(16), + b: [], + c: [], + a: [], + }; + + const serder = new Serder(ked0); + const siger = skp0.sign(b(serder.raw), 0); + assert.equal( + siger.qb64, + 'AAAPkMTS3LrrhVuQB0k4UndDN0xIfEiKYaN7rTlQ_q9ImnBcugwNO8VWTALXzWoaldJEC1IOpEGkEnjZfxxIleoI' + ); + + const ked1 = { + v: 'KERI10JSON000000_', + t: Ilks.vcp, + d: '', + i: '', + s: '0', + bt: toad.toString(16), + b: [], + }; + const vcp = new Serder(ked1); + + const embeds = { + icp: [serder, siger.qb64], + vcp: [vcp, undefined], + }; + + [exn, end] = exchange( + '/multisig/vcp', + {}, + 'test', + '', + dt, + undefined, + undefined, + embeds + ); + + assert.deepStrictEqual(exn.sad, { + a: { + i: '', + }, + d: 'EOK2xNjB5xlSvizCUrkFKbdF4j1nsGpvt6TR1HL0wvaY', + dt: '2023-08-30T17:22:54.183Z', + e: { + d: 'EDPWpKtMoPwro_Of8TQzpNMGdtmfyWzqTcRKQ01fGFRi', + icp: { + a: [], + b: [], + bt: '0', + c: [], + d: '', + i: '', + k: ['DAUDqkmn-hqlQKD8W-FAEa5JUvJC2I9yarEem-AAEg3e'], + kt: '1', + n: ['EAKUR-LmLHWMwXTLWQ1QjxHrihBmwwrV2tYaSG7hOrWj'], + nt: '1', + s: '0', + t: 'icp', + v: 'KERI10JSON0000d3_', + }, + vcp: { + b: [], + bt: '0', + d: '', + i: '', + s: '0', + t: 'vcp', + v: 'KERI10JSON000049_', + }, + }, + i: 'test', + p: '', + q: {}, + r: '/multisig/vcp', + rp: '', + t: 'exn', + v: 'KERI10JSON00021b_', + }); + assert.equal( + d(end), + '-LAZ5AACAA-e-icpAAAPkMTS3LrrhVuQB0k4UndDN0xIfEiKYaN7rTlQ_q9ImnBcugwNO8VWTALXzWoaldJEC1IOpEGkEnjZfxxIleoI' + ); + }); + + it('SendFromEvents', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const exchange = client.exchanges(); + const sith = 1; + const nsith = 1; + const sn = 0; + const toad = 0; + + const raw = new Uint8Array([ + 5, 170, 143, 45, 83, 154, 233, 250, 85, 156, 2, 156, 155, 8, 72, + 117, + ]); + const salter = new Salter({ raw: raw }); + const skp0 = salter.signer( + MtrDex.Ed25519_Seed, + true, + 'A', + Tier.low, + true + ); + const keys = [skp0.verfer.qb64]; + + const skp1 = salter.signer( + MtrDex.Ed25519_Seed, + true, + 'N', + Tier.low, + true + ); + const ndiger = new Diger({}, skp1.verfer.qb64b); + const nxt = [ndiger.qb64]; + assert.deepStrictEqual(nxt, [ + 'EAKUR-LmLHWMwXTLWQ1QjxHrihBmwwrV2tYaSG7hOrWj', + ]); + + const ked0 = { + v: 'KERI10JSON000000_', + t: Ilks.icp, + d: '', + i: '', + s: sn.toString(16), + kt: sith.toString(16), + k: keys, + nt: nsith.toString(16), + n: nxt, + bt: toad.toString(16), + b: [], + c: [], + a: [], + }; + + const serder = new Serder(ked0); + + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + await exchange.sendFromEvents('aid1', '', serder, [''], '', []); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/identifiers/aid1/exchanges'); + assert.equal(lastCall[1]!.method, 'POST'); + }); + + it('Get exchange', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + const client = new SignifyClient(url, bran, Tier.low, boot_url); + await client.boot(); + await client.connect(); + const exchanges = client.exchanges(); + await exchanges.get('EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'); + const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + '/exchanges/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + ); + assert.equal(lastCall[1]!.method, 'GET'); + }); +}); diff --git a/packages/signify-ts/test/app/grouping.test.ts b/packages/signify-ts/test/app/grouping.test.ts new file mode 100644 index 00000000..197da9ad --- /dev/null +++ b/packages/signify-ts/test/app/grouping.test.ts @@ -0,0 +1,52 @@ +import { assert, describe, it } from 'vitest'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import { Tier } from '../../src/keri/core/salter.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { createMockFetch } from './test-utils.ts'; + +const fetchMock = createMockFetch(); + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; + +describe('Grouping', () => { + it('Groups', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const groups = client.groups(); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + await groups.sendRequest('aid1', {}, [], ''); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/identifiers/aid1/multisig/request'); + assert.equal(lastCall[1]!.method, 'POST'); + + await groups.getRequest( + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00' + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal( + lastCall[0]!, + url + + '/multisig/request/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00' + ); + assert.equal(lastCall[1]!.method, 'GET'); + + await groups.join( + 'aid1', + { sad: {} }, + ['sig'], + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', + ['1', '2', '3'], + ['a', 'b', 'c'] + ); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/identifiers/aid1/multisig/join'); + assert.equal(lastCall[1]!.method, 'POST'); + }); +}); diff --git a/packages/signify-ts/test/app/habery.test.ts b/packages/signify-ts/test/app/habery.test.ts new file mode 100644 index 00000000..0c5a2ce4 --- /dev/null +++ b/packages/signify-ts/test/app/habery.test.ts @@ -0,0 +1,57 @@ +import { Habery } from '../../src/keri/app/habery.ts'; +import { assert, describe, it } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Salter } from '../../src/keri/core/salter.ts'; +import { b } from '../../src/keri/core/core.ts'; +import { MtrDex } from '../../src/keri/core/matter.ts'; + +describe('Habery', () => { + it('should manage AID creation and rotation', async () => { + await libsodium.ready; + const salt = new Salter({ raw: b('0123456789abcdef') }).qb64; + const hby = new Habery({ + name: 'signify', + salt: salt, + passcode: '0123456789abcdefghijk', + }); + + assert.equal( + hby.mgr.aeid, + 'BMbZTXzB7LmWPT2TXLGV88PQz5vDEM2L2flUs2yxn3U9' + ); + + const hab = hby.makeHab('test', {}); + + assert.deepStrictEqual(hab.serder.sad['k'], [ + 'DAQVURvW74OJH1Q0C6YLim_tdBYoXABwg6GsAlPaUJXE', + ]); + assert.deepStrictEqual(hab.serder.sad['n'], [ + 'ENBWnU8wNHqq9oqJIimWhxUtNDHReUXtiCwwtjg9zKY0', + ]); + }); + + it('should use passcode as salt', async () => { + await libsodium.ready; + const passcode = '0123456789abcdefghijk'; + if (passcode.length < 21) { + throw new Error('Bran (passcode seed material) too short.'); + } + + const bran = MtrDex.Salt_128 + 'A' + passcode.substring(0, 21); // qb64 salt for seed + const salter = new Salter({ qb64: bran }); + const signer = salter.signer(MtrDex.Ed25519_Seed, true); + assert.equal( + signer.qb64, + 'AKeXgiAUIN7OHGXO6rbw_IzWeaQTr1LF7jWD6YEdrpa6' + ); + assert.equal( + signer.verfer.qb64, + 'DMbZTXzB7LmWPT2TXLGV88PQz5vDEM2L2flUs2yxn3U9' + ); + + const hby = new Habery({ name: 'test', salt: salter.qb64 }); + const hab = hby.makeHab('test', { transferable: true }); + + assert.equal(hab.pre, 'EMRbh7mWJTijcWiQKT3uxozncpa9_gEX1IU0fM1wnKxi'); + }); +}); diff --git a/packages/signify-ts/test/app/notifying.test.ts b/packages/signify-ts/test/app/notifying.test.ts new file mode 100644 index 00000000..885abf20 --- /dev/null +++ b/packages/signify-ts/test/app/notifying.test.ts @@ -0,0 +1,41 @@ +import { assert, describe, it } from 'vitest'; +import { Tier } from '../../src/keri/core/salter.ts'; +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { createMockFetch } from './test-utils.ts'; + +const fetchMock = createMockFetch(); + +const url = 'http://127.0.0.1:3901'; +const boot_url = 'http://127.0.0.1:3903'; + +describe('SignifyClient', () => { + it('Notifications', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const notifications = client.notifications(); + + await notifications.list(20, 40); + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/notifications'); + assert.equal(lastCall[1]!.method, 'GET'); + const lastHeaders = new Headers(lastCall[1]!.headers!); + assert.equal(lastHeaders.get('Range'), 'notes=20-40'); + + await notifications.mark('notificationSAID'); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/notifications/notificationSAID'); + assert.equal(lastCall[1]!.method, 'PUT'); + + await notifications.delete('notificationSAID'); + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + assert.equal(lastCall[0]!, url + '/notifications/notificationSAID'); + assert.equal(lastCall[1]!.method, 'DELETE'); + }); +}); diff --git a/packages/signify-ts/test/app/registry.test.ts b/packages/signify-ts/test/app/registry.test.ts new file mode 100644 index 00000000..8254fdc6 --- /dev/null +++ b/packages/signify-ts/test/app/registry.test.ts @@ -0,0 +1,120 @@ +import { SignifyClient } from '../../src/keri/app/clienting.ts'; +import { anyOfClass, anything, instance, mock, when } from 'ts-mockito'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Registries } from '../../src/keri/app/credentialing.ts'; +import { + Identifier, + IdentifierManagerFactory, + SaltyIdentifierManager, +} from '../../src/index.ts'; +import { assert, describe, expect, it } from 'vitest'; +import { HabState, KeyState } from '../../src/keri/core/keyState.ts'; + +describe('registry', () => { + it('should create a registry', async () => { + await libsodium.ready; + const mockedClient = mock(SignifyClient); + const mockedIdentifiers = mock(Identifier); + const mockedKeyManager = mock(IdentifierManagerFactory); + const mockedKeeper = mock(SaltyIdentifierManager); + + const hab = { + prefix: 'hab prefix', + state: { s: '0', d: 'a digest' } as KeyState, + } as HabState; + + when(mockedClient.manager).thenReturn(instance(mockedKeyManager)); + when(mockedKeyManager.get(hab)).thenReturn(instance(mockedKeeper)); + + when(mockedKeeper.sign(anyOfClass(Uint8Array))).thenResolve([ + 'a signature', + ]); + + when(mockedIdentifiers.get('a name')).thenResolve(hab); + when(mockedClient.identifiers()).thenReturn( + instance(mockedIdentifiers) + ); + + const mockedResponse = mock(Response); + when( + mockedClient.fetch( + '/identifiers/a name/registries', + 'POST', + anything() + ) + ).thenResolve(instance(mockedResponse)); + + const registries = new Registries(instance(mockedClient)); + + const actual = await registries.create({ + name: 'a name', + registryName: 'a registry name', + nonce: '', + }); + + assert.equal( + actual.regser.raw, + '{"v":"KERI10JSON0000c5_","t":"vcp","d":"EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX","i":"EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX","ii":"hab prefix","s":"0","c":["NB"],"bt":"0","b":[],"n":""}' + ); + assert.equal( + actual.serder.raw, + '{"v":"KERI10JSON0000f4_","t":"ixn","d":"EE5R61289Xnpxc2M-euPtsAkp849tUdNJ7DuyBeSiRtm","i":"hab prefix","s":"1","p":"a digest","a":[{"i":"EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX","s":"0","d":"EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX"}]}' + ); + }); + + it('should fail on estanblishmnet only for now', async () => { + await libsodium.ready; + const mockedClient = mock(SignifyClient); + const mockedIdentifiers = mock(Identifier); + const keystate: KeyState = { + s: '0', + d: 'a digest', + i: '', + p: '', + f: '', + dt: '', + et: '', + kt: '', + k: [], + nt: '', + n: [], + bt: '', + b: [], + c: ['EO'], + ee: { + s: '', + d: '', + br: [], + ba: [], + }, + di: '', + }; + const hab = { + prefix: 'hab prefix', + state: keystate, + name: 'a name', + transferable: true, + windexes: [], + icp_dt: '2023-12-01T10:05:25.062609+00:00', + randy: { + prxs: [], + nxts: [], + }, + }; + + when(mockedIdentifiers.get('a name')).thenResolve(hab); + when(mockedClient.identifiers()).thenReturn( + instance(mockedIdentifiers) + ); + + const registries = new Registries(instance(mockedClient)); + + await expect(async () => { + await registries.create({ + name: 'a name', + registryName: 'a registry name', + nonce: '', + }); + }).rejects.toThrowError('establishment only not implemented'); + }); +}); diff --git a/packages/signify-ts/test/app/test-utils.ts b/packages/signify-ts/test/app/test-utils.ts new file mode 100644 index 00000000..e224c546 --- /dev/null +++ b/packages/signify-ts/test/app/test-utils.ts @@ -0,0 +1,367 @@ +import { Mock, vitest } from 'vitest'; +import { + Algos, + Authenticater, + Controller, + CreateIdentiferArgs, + HEADER_SIG_TIME, + IdentifierManagerFactory, + MtrDex, + Salter, + Serials, + Tier, + Vrsn_1_0, + incept, +} from '../../src/index.ts'; +import { + EstablishmentState, + HabState, + KeyState, +} from '../../src/keri/core/keyState.ts'; + +const boot_url = 'http://127.0.0.1:3903'; + +export async function createMockIdentifierState( + name: string, + bran: string, + kargs: CreateIdentiferArgs = {} +): Promise { + const controller = new Controller(bran, Tier.low); + const manager = new IdentifierManagerFactory(controller.salter); + const algo = kargs.algo == undefined ? Algos.salty : kargs.algo; + + const transferable = kargs.transferable ?? true; + const isith = kargs.isith ?? '1'; + const nsith = kargs.nsith ?? '1'; + const wits = kargs.wits ?? []; + const toad = kargs.toad ?? 0; + const dcode = kargs.dcode ?? MtrDex.Blake3_256; + const proxy = kargs.proxy; + const delpre = kargs.delpre; + const data = kargs.data != undefined ? [kargs.data] : []; + const pre = kargs.pre; + const states = kargs.states; + const rstates = kargs.rstates; + const prxs = kargs.prxs; + const nxts = kargs.nxts; + const mhab = kargs.mhab; + const _keys = kargs.keys; + const _ndigs = kargs.ndigs; + const count = kargs.count; + const ncount = kargs.ncount; + const tier = kargs.tier; + const extern_type = kargs.extern_type; + const extern = kargs.extern; + + const keeper = manager!.new(algo, 0, { + transferable: transferable, + isith: isith, + nsith: nsith, + wits: wits, + toad: toad, + proxy: proxy, + delpre: delpre, + dcode: dcode, + data: data, + algo: algo, + pre: pre, + prxs: prxs, + nxts: nxts, + mhab: mhab, + states: states, + rstates: rstates, + keys: _keys, + ndigs: _ndigs, + bran: bran, + count: count, + ncount: ncount, + tier: tier, + extern_type: extern_type, + extern: extern, + }); + const [keys, ndigs] = await keeper!.incept(transferable); + const serder = incept({ + keys: keys!, + isith: isith, + ndigs: ndigs, + nsith: nsith, + toad: toad, + wits: wits, + cnfg: [], + data: data, + version: Vrsn_1_0, + kind: Serials.JSON, + code: dcode, + intive: false, + ...(delpre ? { delpre } : {}), + }); + + return { + name: name, + prefix: serder.pre, + [algo]: keeper.params(), + transferable, + windexes: [], + state: { + vn: [serder.version.major, serder.version.minor], + s: serder.sad.s, + d: serder.sad.d, + i: serder.pre, + ee: serder.sad as EstablishmentState, + kt: serder.sad.kt, + k: serder.sad.k, + nt: serder.sad.nt, + n: serder.sad.n, + bt: serder.sad.bt, + b: serder.sad.b, + p: serder.sad.p ?? '', + f: '', + dt: new Date().toISOString().replace('Z', '000+00:00'), + et: '', + c: [], + di: serder.sad.di ?? '', + } as KeyState, + icp_dt: '2023-12-01T10:05:25.062609+00:00', + } as unknown as HabState; +} + +export const mockConnect = { + agent: { + vn: [1, 0], + i: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + s: '0', + p: '', + d: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + f: '0', + dt: '2023-08-19T21:04:57.948863+00:00', + et: 'dip', + kt: '1', + k: ['DMZh_y-H5C3cSbZZST-fqnsmdNTReZxIh0t2xSTOJQ8a'], + nt: '1', + n: ['EM9M2EQNCBK0MyAhVYBvR98Q0tefpvHgE-lHLs82XgqC'], + bt: '0', + b: [], + c: [], + ee: { + s: '0', + d: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + br: [], + ba: [], + }, + di: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + }, + controller: { + state: { + vn: [1, 0], + i: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + s: '0', + p: '', + d: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + f: '0', + dt: '2023-08-19T21:04:57.959047+00:00', + et: 'icp', + kt: '1', + k: ['DAbWjobbaLqRB94KiAutAHb_qzPpOHm3LURA_ksxetVc'], + nt: '1', + n: ['EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6xZZT_E3h7d2tL'], + bt: '0', + b: [], + c: [], + ee: { + s: '0', + d: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + br: [], + ba: [], + }, + di: '', + }, + ee: { + v: 'KERI10JSON00012b_', + t: 'icp', + d: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + i: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + s: '0', + kt: '1', + k: ['DAbWjobbaLqRB94KiAutAHb_qzPpOHm3LURA_ksxetVc'], + nt: '1', + n: ['EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6xZZT_E3h7d2tL'], + bt: '0', + b: [], + c: [], + a: [], + }, + }, + ridx: 0, + pidx: 0, +}; + +export const mockGetAID = { + name: 'aid1', + prefix: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + salty: { + sxlt: '1AAHnNQTkD0yxOC9tSz_ukbB2e-qhDTStH18uCsi5PCwOyXLONDR3MeKwWv_AVJKGKGi6xiBQH25_R1RXLS2OuK3TN3ovoUKH7-A', + pidx: 0, + kidx: 0, + stem: 'signify:aid', + tier: 'low', + dcode: 'E', + icodes: ['A'], + ncodes: ['A'], + transferable: true, + }, + transferable: true, + state: { + vn: [1, 0], + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: '0', + p: '', + d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + f: '0', + dt: '2023-08-21T22:30:46.473545+00:00', + et: 'icp', + kt: '1', + k: ['DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9'], + nt: '1', + n: ['EAORnRtObOgNiOlMolji-KijC_isa3lRDpHCsol79cOc'], + bt: '0', + b: [], + c: [], + ee: { + s: '0', + d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + br: [], + ba: [], + }, + di: '', + }, + windexes: [], +}; + +export function createMockFetch(): Mock { + const spy = vitest.spyOn(globalThis, 'fetch'); + function resolveUrl(input: unknown) { + if (input instanceof URL) { + return input; + } + + if (typeof input === 'string') { + return new URL(input); + } + + if (input instanceof Request) { + return new URL(input.url); + } + + throw new Error('Invalid URL: ' + input); + } + + function resolveMethod(input: unknown, init?: unknown): string { + if (input instanceof Request) { + return input.method ?? 'GET'; + } + + if ( + init && + typeof init === 'object' && + 'method' in init && + typeof init.method === 'string' + ) { + return init.method ?? 'GET'; + } + + return 'GET'; + } + + spy.mockImplementation(async (input, init) => { + const url = resolveUrl(input); + const method = resolveMethod(input, init); + + if (url.pathname.startsWith('/agent')) { + return Response.json(mockConnect, { status: 202 }); + } else if (url.toString() === boot_url + '/boot') { + return Response.json('', { status: 202 }); + } else { + const headers = new Headers(); + let signed_headers = new Headers(); + + headers.set( + 'Signify-Resource', + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + headers.set( + HEADER_SIG_TIME, + new Date().toISOString().replace('Z', '000+00:00') + ); + headers.set('Content-Type', 'application/json'); + + const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); + const signer = salter.signer( + 'A', + true, + 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', + Tier.low + ); + + const authn = new Authenticater(signer!, signer!.verfer); + + signed_headers = authn.sign( + headers, + method, + + url.pathname.split('?')[0] + ); + + if (url.pathname.startsWith('/credentials')) { + return Response.json(mockCredential, { + status: 200, + headers: signed_headers, + }); + } + + return Response.json(mockGetAID, { + status: 202, + headers: signed_headers, + }); + } + }); + + return spy as Mock; +} + +export const mockCredential = { + sad: { + v: 'ACDC10JSON000197_', + d: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + i: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + ri: 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df', + s: 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', + a: { + d: 'EK0GOjijKd8_RLYz9qDuuG29YbbXjU8yJuTQanf07b6P', + i: 'EKvn1M6shPLnXTb47bugVJblKMuWC0TcLIePP8p98Bby', + dt: '2023-08-23T15:16:07.553000+00:00', + LEI: '5493001KJTIIGC8Y1R17', + }, + }, + pre: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + sadsigers: [ + { + path: '-', + pre: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + sn: 0, + d: 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1', + }, + ], + sadcigars: [], + chains: [], + status: { + v: 'KERI10JSON000135_', + i: 'EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo', + s: '0', + d: 'ENf3IEYwYtFmlq5ZzoI-zFzeR7E3ZNRN2YH_0KAFbdJW', + ri: 'EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df', + ra: {}, + a: { s: 2, d: 'EIpgyKVF0z0Pcn2_HgbWhEKmJhOXFeD4SA62SrxYXOLt' }, + dt: '2023-08-23T15:16:07.553000+00:00', + et: 'iss', + }, +}; diff --git a/packages/signify-ts/test/core/authing.test.ts b/packages/signify-ts/test/core/authing.test.ts new file mode 100644 index 00000000..f1862ac3 --- /dev/null +++ b/packages/signify-ts/test/core/authing.test.ts @@ -0,0 +1,97 @@ +import { assert, describe, it, vitest } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Salter } from '../../src/keri/core/salter.ts'; +import { b } from '../../src/keri/core/core.ts'; +import { Authenticater } from '../../src/keri/core/authing.ts'; +import * as utilApi from '../../src/keri/core/utils.ts'; +import { Verfer } from '../../src/keri/core/verfer.ts'; + +describe('Authenticater.verify', () => { + it('verify signature on Response', async () => { + await libsodium.ready; + const salt = '0123456789abcdef'; + const salter = new Salter({ raw: b(salt) }); + const signer = salter.signer(); + const aaid = 'DMZh_y-H5C3cSbZZST-fqnsmdNTReZxIh0t2xSTOJQ8a'; + const verfer = new Verfer({ qb64: aaid }); + + const headers = new Headers([ + ['Content-Length', '898'], + ['Content-Type', 'application/json'], + [ + 'Signature', + [ + 'indexed="?0"', + 'signify="0BDLh8QCytVBx1YMam4Vt8s4b9HAW1dwfE4yU5H_w1V6gUvPBoVGWQlIMdC16T3WFWHDHCbMcuceQzrr6n9OULsK"', + ].join(';'), + ], + [ + 'Signature-Input', + [ + 'signify=("signify-resource" "@method" "@path" "signify-timestamp")', + 'created=1684715820', + 'keyid="EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei"', + 'alg="ed25519"', + ].join(';'), + ], + [ + 'Signify-Resource', + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + ], + ['Signify-Timestamp', '2023-05-22T00:37:00.248708+00:00'], + ]); + + const authn = new Authenticater(signer, verfer); + assert.notEqual(authn, undefined); + + assert.equal( + authn.verify(new Headers(headers), 'GET', '/identifiers/aid1'), + true + ); + }); +}); + +describe('Authenticater.sign', () => { + it('Create signed headers for a request', async () => { + await libsodium.ready; + const salt = '0123456789abcdef'; + const salter = new Salter({ raw: b(salt) }); + const signer = salter.signer(); + const aaid = 'DDK2N5_fVCWIEO9d8JLhk7hKrkft6MbtkUhaHQsmABHY'; + const verfer = new Verfer({ qb64: aaid }); + + const headers = new Headers([ + ['Content-Type', 'application/json'], + ['Content-Length', '256'], + ['Connection', 'close'], + [ + 'Signify-Resource', + 'EWJkQCFvKuyxZi582yJPb0wcwuW3VXmFNuvbQuBpgmIs', + ], + ['Signify-Timestamp', '2022-09-24T00:05:48.196795+00:00'], + ]); + vitest + .spyOn(utilApi, 'nowUTC') + .mockReturnValue(new Date('2021-01-01T00:00:00.000000+00:00')); + + const authn = new Authenticater(signer, verfer); + const result = authn.sign(headers, 'POST', '/boot'); + + assert.equal(result.has('Signature-Input'), true); + assert.equal(result.has('Signature'), true); + + const expectedSignatureInput = [ + 'signify=("@method" "@path" "signify-resource" "signify-timestamp")', + 'created=1609459200', + 'keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt"', + 'alg="ed25519"', + ].join(';'); + assert.equal(result.get('Signature-Input'), expectedSignatureInput); + + const expectedSignature = [ + 'indexed="?0"', + 'signify="0BChvN_BWAf-mgEuTnWfNnktgHdWOuOh9cWc4o0GFWuZOwra3DyJT5dJ_6BX7AANDOTnIlAKh5Sg_9qGQXHjj5oJ"', + ].join(';'); + assert.equal(result.get('Signature'), expectedSignature); + }); +}); diff --git a/packages/signify-ts/test/core/base64.test.ts b/packages/signify-ts/test/core/base64.test.ts new file mode 100644 index 00000000..17f5c109 --- /dev/null +++ b/packages/signify-ts/test/core/base64.test.ts @@ -0,0 +1,40 @@ +import { assert, test } from 'vitest'; +import { + decodeBase64Url, + encodeBase64Url, +} from '../../src/keri/core/base64.ts'; + +test('encode', () => { + assert.equal(encodeBase64Url(Uint8Array.from([102])), 'Zg'); + assert.equal(encodeBase64Url(Uint8Array.from([102, 105])), 'Zmk'); + assert.equal(encodeBase64Url(Uint8Array.from([102, 105, 115])), 'Zmlz'); + assert.equal( + encodeBase64Url(Uint8Array.from([102, 105, 115, 104])), + 'ZmlzaA' + ); + assert.equal(encodeBase64Url(Uint8Array.from([248])), '-A'); + assert.equal(encodeBase64Url(Uint8Array.from([252])), '_A'); +}); + +test('decode', () => { + assert.deepEqual(decodeBase64Url('Zg'), Uint8Array.from([102])); + assert.deepEqual(decodeBase64Url('Zmk'), Uint8Array.from([102, 105])); + assert.deepEqual(decodeBase64Url('Zmlz'), Uint8Array.from([102, 105, 115])); + assert.deepEqual( + decodeBase64Url('ZmlzaA'), + Uint8Array.from([102, 105, 115, 104]) + ); + assert.deepEqual(Uint8Array.from([248]), decodeBase64Url('-A')); + assert.deepEqual(Uint8Array.from([252]), decodeBase64Url('_A')); +}); + +test('Test encode / decode compare with built in node Buffer', () => { + const text = '🏳️🏳️'; + const b64url = '8J-Ps--4j_Cfj7PvuI8'; + const data = Uint8Array.from([ + 240, 159, 143, 179, 239, 184, 143, 240, 159, 143, 179, 239, 184, 143, + ]); + + assert.deepEqual(b64url, encodeBase64Url(new TextEncoder().encode(text))); + assert.deepEqual(data, decodeBase64Url(b64url)); +}); diff --git a/packages/signify-ts/test/core/bexter.test.ts b/packages/signify-ts/test/core/bexter.test.ts new file mode 100644 index 00000000..863f252c --- /dev/null +++ b/packages/signify-ts/test/core/bexter.test.ts @@ -0,0 +1,114 @@ +import { assert, describe, it } from 'vitest'; +import { Bexter } from '../../src/keri/core/bexter.ts'; +import { b, MtrDex } from '../../src/index.ts'; + +describe('Bexter', () => { + it('should bext-ify stuff (and back again)', () => { + assert.throws(() => { + new Bexter({}); + }); + + let bext = '@!'; + assert.throws(() => { + new Bexter({}, bext); + }); + + bext = ''; + let bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L0); + assert.equal(bexter.both, '4AAA'); + assert.deepStrictEqual(bexter.raw, b('')); + assert.equal(bexter.qb64, '4AAA'); + assert.equal(bexter.bext, bext); + + bext = '-'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L2); + assert.equal(bexter.both, '6AAB'); + assert.deepStrictEqual(bexter.raw, b('>')); + assert.equal(bexter.qb64, '6AABAAA-'); + assert.equal(bexter.bext, bext); + + bext = '-A'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L1); + assert.equal(bexter.both, '5AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([15, 128])); + assert.equal(bexter.qb64, '5AABAA-A'); + assert.equal(bexter.bext, bext); + + bext = '-A-'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L0); + assert.equal(bexter.both, '4AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([3, 224, 62])); + assert.equal(bexter.qb64, '4AABA-A-'); + assert.equal(bexter.bext, bext); + + bext = '-A-B'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L0); + assert.equal(bexter.both, '4AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([248, 15, 129])); + assert.equal(bexter.qb64, '4AAB-A-B'); + assert.equal(bexter.bext, bext); + + bext = 'A'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L2); + assert.equal(bexter.both, '6AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([0])); + assert.equal(bexter.qb64, '6AABAAAA'); + assert.equal(bexter.bext, bext); + + bext = 'AA'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L1); + assert.equal(bexter.both, '5AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([0, 0])); + assert.equal(bexter.qb64, '5AABAAAA'); + assert.equal(bexter.bext, bext); + + bext = 'AAA'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L0); + assert.equal(bexter.both, '4AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([0, 0, 0])); + assert.equal(bexter.qb64, '4AABAAAA'); + assert.equal(bexter.bext, bext); + + bext = 'AAAA'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L0); + assert.equal(bexter.both, '4AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([0, 0, 0])); + assert.equal(bexter.qb64, '4AABAAAA'); + assert.equal(bexter.bext, 'AAA'); + assert.notEqual(bexter.bext, bext); + + bext = 'ABB'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L0); + assert.equal(bexter.both, '4AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([0, 0, 65])); + assert.equal(bexter.qb64, '4AABAABB'); + assert.equal(bexter.bext, bext); + + bext = 'BBB'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L0); + assert.equal(bexter.both, '4AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([0, 16, 65])); + assert.equal(bexter.qb64, '4AABABBB'); + assert.equal(bexter.bext, bext); + + bext = 'ABBB'; + bexter = new Bexter({}, bext); + assert.equal(bexter.code, MtrDex.StrB64_L0); + assert.equal(bexter.both, '4AAB'); + assert.deepStrictEqual(bexter.raw, Uint8Array.from([0, 16, 65])); + assert.equal(bexter.qb64, '4AABABBB'); + assert.equal(bexter.bext, 'BBB'); + assert.notEqual(bexter.bext, bext); + }); +}); diff --git a/packages/signify-ts/test/core/coring.test.ts b/packages/signify-ts/test/core/coring.test.ts new file mode 100644 index 00000000..b0e4e613 --- /dev/null +++ b/packages/signify-ts/test/core/coring.test.ts @@ -0,0 +1,121 @@ +import libsodium from 'libsodium-wrappers-sumo'; +import { + b, + d, + b64ToInt, + intToB64, + intToB64b, +} from '../../src/keri/core/core.ts'; +import { assert, describe, it } from 'vitest'; +import { bytesToInt, intToBytes } from '../../src/keri/core/utils.ts'; + +describe('int to b64 and back', () => { + it('should encode and decode stuff', async () => { + await libsodium.ready; + + let cs = intToB64(0); + assert.equal(cs, 'A'); + let i = b64ToInt(cs); + assert.equal(i, 0); + + cs = intToB64(0, 0); + assert.equal(cs, ''); + assert.throws(() => { + i = b64ToInt(cs); + }); + + assert.throws(() => { + i = b64ToInt(cs); + }); + + let csb = intToB64b(0); + assert.deepStrictEqual(csb, b('A')); + i = b64ToInt(d(csb)); + assert.equal(i, 0); + + cs = intToB64(27); + assert.equal(cs, 'b'); + i = b64ToInt(cs); + assert.equal(i, 27); + + csb = intToB64b(27); + assert.deepStrictEqual(csb, b('b')); + i = b64ToInt(d(csb)); + assert.equal(i, 27); + + cs = intToB64(27, 2); + assert.equal(cs, 'Ab'); + i = b64ToInt(cs); + assert.equal(i, 27); + + csb = intToB64b(27, 2); + assert.deepStrictEqual(csb, b('Ab')); + i = b64ToInt(d(csb)); + assert.equal(i, 27); + + cs = intToB64(80); + assert.equal(cs, 'BQ'); + i = b64ToInt(cs); + assert.equal(i, 80); + + csb = intToB64b(80); + assert.deepStrictEqual(csb, b('BQ')); + i = b64ToInt(d(csb)); + assert.equal(i, 80); + + cs = intToB64(4095); + assert.equal(cs, '__'); + i = b64ToInt(cs); + assert.equal(i, 4095); + + csb = intToB64b(4095); + assert.deepStrictEqual(csb, b('__')); + i = b64ToInt(d(csb)); + assert.equal(i, 4095); + + cs = intToB64(4096); + assert.equal(cs, 'BAA'); + i = b64ToInt(cs); + assert.equal(i, 4096); + + csb = intToB64b(4096); + assert.deepStrictEqual(csb, b('BAA')); + i = b64ToInt(d(csb)); + assert.equal(i, 4096); + + cs = intToB64(6011); + assert.equal(cs, 'Bd7'); + i = b64ToInt(cs); + assert.equal(i, 6011); + + csb = intToB64b(6011); + assert.deepStrictEqual(csb, b('Bd7')); + i = b64ToInt(d(csb)); + assert.equal(i, 6011); + }); +}); + +describe('int to bytes and back', () => { + it('should encode and decode stuff', async () => { + let b = intToBytes(0, 8); + let n = bytesToInt(b); + assert.equal(n, 0); + b = intToBytes(1, 8); + n = bytesToInt(b); + assert.equal(n, 1); + + b = intToBytes(0, 16); + n = bytesToInt(b); + assert.equal(n, 0); + b = intToBytes(1, 16); + n = bytesToInt(b); + assert.equal(n, 1); + + b = intToBytes(0, 2); + n = bytesToInt(b); + assert.equal(n, 0); + b = intToBytes(1, 2); + n = bytesToInt(b); + assert.equal(n, 1); + }); +}); diff --git a/packages/signify-ts/test/core/counter.test.ts b/packages/signify-ts/test/core/counter.test.ts new file mode 100644 index 00000000..cd4a0e80 --- /dev/null +++ b/packages/signify-ts/test/core/counter.test.ts @@ -0,0 +1,155 @@ +import { Counter, CtrDex } from '../../src/keri/core/counter.ts'; +import { assert, describe, it } from 'vitest'; +import { b, b64ToInt, intToB64 } from '../../src/keri/core/core.ts'; + +describe('int to b64 and back', () => { + it('should encode and decode stuff', async () => { + assert.equal(Counter.Sizes.get('-A')!.hs, 2); // hard size + assert.equal(Counter.Sizes.get('-A')!.ss, 2); // soft size + assert.equal(Counter.Sizes.get('-A')!.fs, 4); // full size + assert.equal(Counter.Sizes.get('-A')!.ls, 0); // lead size + + // verify first hs Sizes matches hs in Codes for same first char + Counter.Sizes.forEach((_, ckey) => { + const key = ckey.slice(0, 2); + assert.equal(Counter.Hards.get(key), Counter.Sizes.get(ckey)!.hs); + }); + + // verify all Codes have hs > 0 and ss > 0 and fs = hs + ss and not fs % 4 + Counter.Sizes.forEach((val) => { + assert.equal( + val.hs > 0 && + val.ss > 0 && + val.hs + val.ss == val.fs && + !(val.fs % 4), + true + ); + }); + // Bizes maps bytes of sextet of decoded first character of code with hard size of code + // verify equivalents of items for Sizes and Bizes + // Counter.Hards.forEach((sval, skey) => { + // let ckey = codeB64ToB2(skey) + // assert.equal(Counter.Bards[ckey], sval) + // }) + + assert.throws(() => { + new Counter({}); + }); + + let count = 1; + let qsc = CtrDex.ControllerIdxSigs + intToB64(count, 2); + assert.equal(qsc, '-AAB'); + let qscb = b(qsc); + + let counter = new Counter({ code: CtrDex.ControllerIdxSigs }); // default count = 1 + assert.equal(counter.code, CtrDex.ControllerIdxSigs); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + counter = new Counter({ qb64: qsc }); // default count = 1 + assert.equal(counter.code, CtrDex.ControllerIdxSigs); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + counter = new Counter({ qb64b: qscb }); // default count = 1 + assert.equal(counter.code, CtrDex.ControllerIdxSigs); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + const longqs64 = `${qsc}ABCD`; + counter = new Counter({ qb64: longqs64 }); + assert.equal(counter.qb64.length, Counter.Sizes.get(counter.code)!.fs); + + const shortqcs = qsc.slice(0, -1); + assert.throws(() => { + new Counter({ qb64: shortqcs }); + }); + + count = 5; + qsc = CtrDex.ControllerIdxSigs + intToB64(count, 2); + assert.equal(qsc, '-AAF'); + qscb = b(qsc); + + counter = new Counter({ code: CtrDex.ControllerIdxSigs, count: count }); + assert.equal(counter.code, CtrDex.ControllerIdxSigs); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + counter = new Counter({ qb64: qsc }); // default count = 1 + assert.equal(counter.code, CtrDex.ControllerIdxSigs); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + counter = new Counter({ qb64b: qscb }); // default count = 1 + assert.equal(counter.code, CtrDex.ControllerIdxSigs); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + // test with big codes index=1024 + count = 1024; + qsc = CtrDex.BigAttachedMaterialQuadlets + intToB64(count, 5); + assert.equal(qsc, '-0VAAAQA'); + qscb = b(qsc); + + counter = new Counter({ + code: CtrDex.BigAttachedMaterialQuadlets, + count: count, + }); + assert.equal(counter.code, CtrDex.BigAttachedMaterialQuadlets); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + counter = new Counter({ qb64: qsc }); // default count = 1 + assert.equal(counter.code, CtrDex.BigAttachedMaterialQuadlets); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + counter = new Counter({ qb64b: qscb }); // default count = 1 + assert.equal(counter.code, CtrDex.BigAttachedMaterialQuadlets); + assert.equal(counter.count, count); + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + const verint = 0; + const version = intToB64(verint, 3); + assert.equal(version, 'AAA'); + assert.equal(verint, b64ToInt(version)); + qsc = CtrDex.KERIProtocolStack + version; + assert.equal(qsc, '--AAAAAA'); // keri Cesr version 0.0.0 + qscb = b(qsc); + + counter = new Counter({ + code: CtrDex.KERIProtocolStack, + count: verint, + }); + assert.equal(counter.code, CtrDex.KERIProtocolStack); + assert.equal(counter.count, verint); + assert.equal(counter.countToB64(3), version); + assert.equal(counter.countToB64(), version); // default length + assert.deepStrictEqual(counter.qb64b, qscb); + assert.equal(counter.qb64, qsc); + + assert.equal(Counter.semVerToB64('1.2.3'), 'BCD'); + assert.equal(Counter.semVerToB64(), 'AAA'); + assert.equal(Counter.semVerToB64('', 1), 'BAA'); + assert.equal(Counter.semVerToB64('', 0, 1), 'ABA'); + assert.equal(Counter.semVerToB64('', 0, 0, 1), 'AAB'); + assert.equal(Counter.semVerToB64('', 3, 4, 5), 'DEF'); + + assert.equal(Counter.semVerToB64('1.1'), 'BBA'); + assert.equal(Counter.semVerToB64('1.'), 'BAA'); + assert.equal(Counter.semVerToB64('1'), 'BAA'); + assert.equal(Counter.semVerToB64('1.2.'), 'BCA'); + assert.equal(Counter.semVerToB64('..'), 'AAA'); + assert.equal(Counter.semVerToB64('1..3'), 'BAD'); + assert.equal(Counter.semVerToB64('4', 1, 2, 3), 'ECD'); + }); +}); diff --git a/packages/signify-ts/test/core/decrypter.test.ts b/packages/signify-ts/test/core/decrypter.test.ts new file mode 100644 index 00000000..ec4ea2a2 --- /dev/null +++ b/packages/signify-ts/test/core/decrypter.test.ts @@ -0,0 +1,169 @@ +import libsodium from 'libsodium-wrappers-sumo'; +import { Signer } from '../../src/keri/core/signer.ts'; +import { Matter, MtrDex } from '../../src/keri/core/matter.ts'; +import { assert, describe, it } from 'vitest'; +import { Salter } from '../../src/keri/core/salter.ts'; +import { Decrypter } from '../../src/keri/core/decrypter.ts'; +import { Encrypter } from '../../src/keri/core/encrypter.ts'; +import { b } from '../../src/keri/core/core.ts'; + +describe('Decrypter', () => { + it('should decrypt stuff', async () => { + await libsodium.ready; + + // (b'\x18;0\xc4\x0f*vF\xfa\xe3\xa2Eee\x1f\x96o\xce)G\x85\xe3X\x86\xda\x04\xf0\xdc\xde\x06\xc0+') + const seed = new Uint8Array([ + 24, 59, 48, 196, 15, 42, 118, 70, 250, 227, 162, 69, 101, 101, 31, + 150, 111, 206, 41, 71, 133, 227, 88, 134, 218, 4, 240, 220, 222, 6, + 192, 43, + ]); + + const signer = new Signer({ raw: seed, code: MtrDex.Ed25519_Seed }); + assert.equal(signer.verfer.code, MtrDex.Ed25519); + assert.equal(signer.verfer.transferable, true); // default + const seedqb64 = signer.qb64; + const seedqb64b = signer.qb64b; + assert.equal(seedqb64, 'ABg7MMQPKnZG-uOiRWVlH5ZvzilHheNYhtoE8NzeBsAr'); + + // also works for Matter + assert.deepStrictEqual( + seedqb64b, + new Matter({ raw: seed, code: MtrDex.Ed25519_Seed }).qb64b + ); + + // raw = b'6\x08d\r\xa1\xbb9\x8dp\x8d\xa0\xc0\x13J\x87r' + const raw = new Uint8Array([ + 54, 8, 100, 13, 161, 187, 57, 141, 112, 141, 160, 192, 19, 74, 135, + 114, + ]); + const salter = new Salter({ raw: raw, code: MtrDex.Salt_128 }); + assert.equal(salter.code, MtrDex.Salt_128); + const saltqb64 = salter.qb64; + const saltqb64b = salter.qb64b; + assert.deepStrictEqual(saltqb64, '0AA2CGQNobs5jXCNoMATSody'); + + // also works for Matter + assert.deepStrictEqual( + saltqb64b, + new Matter({ raw: raw, code: MtrDex.Salt_128 }).qb64b + ); + + // cryptseed = b'h,#|\x8ap"\x12\xc43t2\xa6\xe1\x18\x19\xf0f2,y\xc4\xc21@\xf5@\x15.\xa2\x1a\xcf' + const cryptseed = new Uint8Array([ + 104, 44, 35, 124, 138, 112, 34, 18, 196, 51, 116, 50, 166, 225, 24, + 25, 240, 102, 50, 44, 121, 196, 194, 49, 64, 245, 64, 21, 46, 162, + 26, 207, + ]); + const cryptsigner = new Signer({ + raw: cryptseed, + code: MtrDex.Ed25519_Seed, + transferable: true, + }); + const keypair = libsodium.crypto_sign_seed_keypair(cryptseed); // raw + const pubkey = libsodium.crypto_sign_ed25519_pk_to_curve25519( + keypair.publicKey + ); + const prikey = libsodium.crypto_sign_ed25519_sk_to_curve25519( + keypair.privateKey + ); + + assert.throws(function () { + new Decrypter({}); + }); + + // create encrypter + const encrypter = new Encrypter({ raw: pubkey }); + assert.equal(encrypter.code, MtrDex.X25519); + assert.equal( + encrypter.qb64, + 'CAF7Wr3XNq5hArcOuBJzaY6Nd23jgtUVI6KDfb3VngkR' + ); + assert.deepStrictEqual(encrypter.raw, pubkey); + + // create cipher of seed + const seedcipher = encrypter.encrypt(seedqb64b); + assert.equal(seedcipher.code, MtrDex.X25519_Cipher_Seed); + // each encryption uses a nonce so not a stable representation for testing + + // create decrypter from prikey + let decrypter = new Decrypter({ raw: prikey }); + assert.equal(decrypter.code, MtrDex.X25519_Private); + assert.equal( + decrypter.qb64, + 'OLCFxqMz1z1UUS0TEJnvZP_zXHcuYdQsSGBWdOZeY5VQ' + ); + assert.deepStrictEqual(decrypter.raw, prikey); + + // decrypt seed cipher using ser + let designer = decrypter.decrypt( + seedcipher.qb64b, + null, + signer.verfer.transferable + ); + assert.deepStrictEqual(designer.qb64b, seedqb64b); + assert.equal(designer.code, MtrDex.Ed25519_Seed); + assert.equal(designer.verfer.code, MtrDex.Ed25519); + assert.equal(signer.verfer.transferable, true); + + // decrypt seed cipher using cipher + designer = decrypter.decrypt( + null, + seedcipher, + signer.verfer.transferable + ); + assert.deepStrictEqual(designer.qb64b, seedqb64b); + assert.equal(designer.code, MtrDex.Ed25519_Seed); + assert.equal(designer.verfer.code, MtrDex.Ed25519); + assert.equal(signer.verfer.transferable, true); + + // create cipher of salt + const saltcipher = encrypter.encrypt(saltqb64b); + assert.equal(saltcipher.code, MtrDex.X25519_Cipher_Salt); + // each encryption uses a nonce so not a stable representation for testing + + // decrypt salt cipher using ser + let desalter = decrypter.decrypt(saltcipher.qb64b); + assert.deepStrictEqual(desalter.qb64b, saltqb64b); + assert.equal(desalter.code, MtrDex.Salt_128); + + // decrypt salt cipher using cipher + desalter = decrypter.decrypt(null, saltcipher); + assert.deepStrictEqual(desalter.qb64b, saltqb64b); + assert.equal(desalter.code, MtrDex.Salt_128); + + // use previously stored fully qualified seed cipher with different nonce + // get from seedcipher above + const cipherseed = + 'PM9jOGWNYfjM_oLXJNaQ8UlFSAV5ACjsUY7J16xfzrlpc9Ve3A5WYrZ4o_NHtP5lhp78Usspl9fyFdnCdItNd5JyqZ6dt8SXOt6TOqOCs-gy0obrwFkPPqBvVkEw'; + designer = decrypter.decrypt( + b(cipherseed), + null, + signer.verfer.transferable + ); + assert.deepStrictEqual(designer.qb64b, seedqb64b); + assert.equal(designer.code, MtrDex.Ed25519_Seed); + assert.equal(designer.verfer.code, MtrDex.Ed25519); + + // use previously stored fully qualified salt cipher with different nonce + // get from saltcipher above + const ciphersalt = + '1AAHjlR2QR9J5Et67Wy-ZaVdTryN6T6ohg44r73GLRPnHw-5S3ABFkhWyIwLOI6TXUB_5CT13S8JvknxLxBaF8ANPK9FSOPD8tYu'; + desalter = decrypter.decrypt(b(ciphersalt)); + assert.deepStrictEqual(desalter.qb64b, saltqb64b); + assert.equal(desalter.code, MtrDex.Salt_128); + + // Create new decrypter but use seed parameter to init prikey + decrypter = new Decrypter({}, cryptsigner.qb64b); + assert.equal(decrypter.code, MtrDex.X25519_Private); + assert.equal( + decrypter.qb64, + 'OLCFxqMz1z1UUS0TEJnvZP_zXHcuYdQsSGBWdOZeY5VQ' + ); + assert.deepStrictEqual(decrypter.raw, prikey); + + // decrypt ciphersalt + desalter = decrypter.decrypt(saltcipher.qb64b); + assert.deepStrictEqual(desalter.qb64b, saltqb64b); + assert.equal(desalter.code, MtrDex.Salt_128); + }); +}); diff --git a/packages/signify-ts/test/core/diger.test.ts b/packages/signify-ts/test/core/diger.test.ts new file mode 100644 index 00000000..e3818dfa --- /dev/null +++ b/packages/signify-ts/test/core/diger.test.ts @@ -0,0 +1,53 @@ +import { Matter } from '../../src/keri/core/matter.ts'; +import { assert, describe, it } from 'vitest'; +import { blake3 } from '@noble/hashes/blake3'; + +import { Diger } from '../../src/keri/core/diger.ts'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import { concat } from '../../src/keri/core/core.ts'; + +function encodeText(text: string) { + return new TextEncoder().encode(text); +} + +describe('Diger', () => { + it('should generate digests', () => { + // Create something to digest and verify + const ser = encodeText('abcdefghijklmnopqrstuvwxyz0123456789'); + + const digest = blake3.create({ dkLen: 32 }).update(ser).digest(); + + let diger = new Diger({ raw: digest }); + assert.deepStrictEqual(diger.code, MtrDex.Blake3_256); + + let sizage = Matter.Sizes.get(diger.code); + assert.deepStrictEqual(diger.qb64.length, sizage!.fs); + let result = diger.verify(ser); + assert.equal(result, true); + + result = diger.verify(concat(ser, encodeText('2j2idjpwjfepjtgi'))); + assert.equal(result, false); + diger = new Diger({ raw: digest, code: MtrDex.Blake3_256 }); + assert.deepStrictEqual(diger.code, MtrDex.Blake3_256); + + assert.equal( + diger.qb64, + 'ELC5L3iBVD77d_MYbYGGCUQgqQBju1o4x1Ud-z2sL-ux' + ); + sizage = Matter.Sizes.get(diger.code); + assert.deepStrictEqual(diger.qb64.length, sizage!.fs); + + result = diger.verify(ser); + assert.equal(result, true); + + diger = new Diger({}, ser); + assert.equal( + diger.qb64, + 'ELC5L3iBVD77d_MYbYGGCUQgqQBju1o4x1Ud-z2sL-ux' + ); + sizage = Matter.Sizes.get(diger.code); + assert.deepStrictEqual(diger.qb64.length, sizage!.fs); + result = diger.verify(ser); + assert.equal(result, true); + }); +}); diff --git a/packages/signify-ts/test/core/encrypter.test.ts b/packages/signify-ts/test/core/encrypter.test.ts new file mode 100644 index 00000000..3e3c54fa --- /dev/null +++ b/packages/signify-ts/test/core/encrypter.test.ts @@ -0,0 +1,102 @@ +import { Matter } from '../../src/keri/core/matter.ts'; + +import { assert, describe, it } from 'vitest'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Signer } from '../../src/keri/core/signer.ts'; +import { Encrypter } from '../../src/keri/core/encrypter.ts'; +import { Verfer } from '../../src/keri/core/verfer.ts'; +import { d } from '../../src/keri/core/core.ts'; + +describe('Encrypter', () => { + it('should encrypt stuff', async () => { + await libsodium.ready; + + // (b'\x18;0\xc4\x0f*vF\xfa\xe3\xa2Eee\x1f\x96o\xce)G\x85\xe3X\x86\xda\x04\xf0\xdc\xde\x06\xc0+') + const seed = new Uint8Array([ + 24, 59, 48, 196, 15, 42, 118, 70, 250, 227, 162, 69, 101, 101, 31, + 150, 111, 206, 41, 71, 133, 227, 88, 134, 218, 4, 240, 220, 222, 6, + 192, 43, + ]); + const seedqb64b = new Matter({ raw: seed, code: MtrDex.Ed25519_Seed }) + .qb64b; + + assert.equal( + d(seedqb64b), + 'ABg7MMQPKnZG-uOiRWVlH5ZvzilHheNYhtoE8NzeBsAr' + ); + + // b'6\x08d\r\xa1\xbb9\x8dp\x8d\xa0\xc0\x13J\x87r' + const salt = new Uint8Array([ + 54, 8, 100, 13, 161, 187, 57, 141, 112, 141, 160, 192, 19, 74, 135, + 114, + ]); + const saltqb64 = new Matter({ raw: salt, code: MtrDex.Salt_128 }).qb64; + const saltqb64b = new Matter({ raw: salt, code: MtrDex.Salt_128 }) + .qb64b; + assert.equal(saltqb64, '0AA2CGQNobs5jXCNoMATSody'); + + // b'h,#|\x8ap"\x12\xc43t2\xa6\xe1\x18\x19\xf0f2,y\xc4\xc21@\xf5@\x15.\xa2\x1a\xcf' + const cryptseed = new Uint8Array([ + 104, 44, 35, 124, 138, 112, 34, 18, 196, 51, 116, 50, 166, 225, 24, + 25, 240, 102, 50, 44, 121, 196, 194, 49, 64, 245, 64, 21, 46, 162, + 26, 207, + ]); + const cryptsigner = new Signer({ + raw: cryptseed, + code: MtrDex.Ed25519_Seed, + transferable: true, + }); + const keypair = libsodium.crypto_sign_seed_keypair(cryptseed); // raw + const pubkey = libsodium.crypto_sign_ed25519_pk_to_curve25519( + keypair.publicKey + ); + const prikey = libsodium.crypto_sign_ed25519_sk_to_curve25519( + keypair.privateKey + ); + + assert.throws(function () { + new Encrypter({}); + }); + + let encrypter = new Encrypter({ raw: pubkey }); + assert.equal(encrypter.code, MtrDex.X25519); + assert.equal( + encrypter.qb64, + 'CAF7Wr3XNq5hArcOuBJzaY6Nd23jgtUVI6KDfb3VngkR' + ); + assert.deepStrictEqual(encrypter.raw, pubkey); + assert.equal(encrypter.verifySeed(cryptsigner.qb64b), true); + + let cipher = encrypter.encrypt(seedqb64b); + assert.equal(cipher.code, MtrDex.X25519_Cipher_Seed); + let uncb = libsodium.crypto_box_seal_open( + cipher.raw, + encrypter.raw, + prikey + ); + assert.deepStrictEqual(uncb, seedqb64b); + + cipher = encrypter.encrypt(saltqb64b); + assert.equal(cipher.code, MtrDex.X25519_Cipher_Salt); + uncb = libsodium.crypto_box_seal_open( + cipher.raw, + encrypter.raw, + prikey + ); + assert.deepStrictEqual(uncb, saltqb64b); + + const verfer = new Verfer({ + raw: keypair.publicKey, + code: MtrDex.Ed25519, + }); + + encrypter = new Encrypter({}, verfer.qb64b); + assert.equal(encrypter.code, MtrDex.X25519); + assert.equal( + encrypter.qb64, + 'CAF7Wr3XNq5hArcOuBJzaY6Nd23jgtUVI6KDfb3VngkR' + ); + assert.deepStrictEqual(encrypter.raw, pubkey); + }); +}); diff --git a/packages/signify-ts/test/core/eventing.test.ts b/packages/signify-ts/test/core/eventing.test.ts new file mode 100644 index 00000000..267ae080 --- /dev/null +++ b/packages/signify-ts/test/core/eventing.test.ts @@ -0,0 +1,219 @@ +import libsodium from 'libsodium-wrappers-sumo'; +import { Signer } from '../../src/keri/core/signer.ts'; +import { assert, describe, it } from 'vitest'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import { incept, messagize, rotate } from '../../src/keri/core/eventing.ts'; +import { Saider } from '../../src/keri/core/saider.ts'; +import { Diger } from '../../src/keri/core/diger.ts'; +import { b, d, Ilks } from '../../src/keri/core/core.ts'; +import { Siger } from '../../src/keri/core/siger.ts'; + +describe('key event function', () => { + it('incept should create inception events', async () => { + await libsodium.ready; + + const seed = new Uint8Array([ + 159, 123, 168, 167, 168, 67, 57, 150, 38, 250, 177, 153, 235, 170, + 32, 196, 27, 71, 17, 196, 174, 83, 65, 82, 201, 189, 4, 157, 133, + 41, 126, 147, + ]); + let signer0 = new Signer({ raw: seed, transferable: false }); // original signing keypair non transferable + assert.equal(signer0.code, MtrDex.Ed25519_Seed); + assert.equal(signer0.verfer.code, MtrDex.Ed25519N); + let keys0 = [signer0.verfer.qb64]; + let serder = incept({ keys: keys0 }); // default nxt is empty so abandoned + assert.equal( + serder.sad['i'], + 'BFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH' + ); + assert.deepStrictEqual(serder.sad['n'], []); + assert.equal( + serder.raw, + '{"v":"KERI10JSON0000fd_","t":"icp","d":"EMW0zK3bagYPO6gx3w7Ua90f-I7x5kGIaI4X' + + 'eq9W8_As","i":"BFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH","s":"0","kt":"1' + + '","k":["BFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH"],"nt":"0","n":[],"bt":' + + '"0","b":[],"c":[],"a":[]}' + ); + let saider = new Saider({ code: MtrDex.Blake3_256 }, serder.sad); + assert.equal(saider.verify(serder.sad), true); + + assert.throws(() => { + serder = incept({ + keys: keys0, + code: MtrDex.Ed25519N, + ndigs: ['ABCDE'], + }); + }); + + assert.throws(() => { + serder = incept({ + keys: keys0, + code: MtrDex.Ed25519N, + wits: ['ABCDE'], + }); + }); + + assert.throws(() => { + serder = incept({ + keys: keys0, + code: MtrDex.Ed25519N, + data: [{ i: 'ABCDE' }], + }); + }); + + signer0 = new Signer({ raw: seed }); // original signing keypair transferable default + assert.equal(signer0.code, MtrDex.Ed25519_Seed); + assert.equal(signer0.verfer.code, MtrDex.Ed25519); + keys0 = [signer0.verfer.qb64]; + serder = incept({ keys: keys0 }); // default nxt is empty so abandoned + assert.equal( + serder.sad['i'], + 'DFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH' + ); + assert.deepStrictEqual(serder.sad['n'], []); + assert.equal( + serder.raw, + '{"v":"KERI10JSON0000fd_","t":"icp","d":"EPLRRJFe2FHdXKVTkSEX4xb4x-YaPFJ2Xds1' + + 'vhtNTd4n","i":"DFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH","s":"0","kt":"1' + + '","k":["DFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH"],"nt":"0","n":[],"bt":' + + '"0","b":[],"c":[],"a":[]}' + ); + saider = new Saider({ code: MtrDex.Blake3_256 }, serder.sad); + assert.equal(saider.verify(serder.sad), true); + + // (b'\x83B~\x04\x94\xe3\xceUQy\x11f\x0c\x93]\x1e\xbf\xacQ\xb5\xd6Y^\xa2E\xfa\x015\x98Y\xdd\xe8') + let seed1 = new Uint8Array([ + 131, 66, 126, 4, 148, 227, 206, 85, 81, 121, 17, 102, 12, 147, 93, + 30, 191, 172, 81, 181, 214, 89, 94, 162, 69, 250, 1, 53, 152, 89, + 221, 232, + ]); + let signer1 = new Signer({ raw: seed1 }); // next signing keypair transferable is default + assert.equal(signer1.code, MtrDex.Ed25519_Seed); + assert.equal(signer1.verfer.code, MtrDex.Ed25519); + // compute nxt digest + let nxt1 = [new Diger({}, signer1.verfer.qb64b).qb64]; // dfault sith is 1 + assert.deepStrictEqual(nxt1, [ + 'EIf-ENw7PrM52w4H-S7NGU2qVIfraXVIlV9hEAaMHg7W', + ]); + let serder0 = incept({ + keys: keys0, + ndigs: nxt1, + code: MtrDex.Blake3_256, + }); // intive false + assert.equal(serder0.sad['t'], Ilks.icp); + assert.equal( + serder0.sad['d'], + 'EAKCxMOuoRzREVHsHCkLilBrUXTvyenBiuM2QtV8BB0C' + ); + assert.equal(serder0.sad['d'], serder0.sad['i']); + assert.equal(serder0.sad['s'], '0'); + assert.equal(serder0.sad['kt'], '1'); + assert.equal(serder0.sad['nt'], '1'); + assert.deepStrictEqual(serder0.sad['n'], nxt1); + assert.equal(serder0.sad['bt'], '0'); // hex str + assert.equal( + serder0.raw, + '{"v":"KERI10JSON00012b_","t":"icp","d":"EAKCxMOuoRzREVHsHCkLilBrUXTvyenBiuM2' + + 'QtV8BB0C","i":"EAKCxMOuoRzREVHsHCkLilBrUXTvyenBiuM2QtV8BB0C","s":"0","kt":"1' + + '","k":["DFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH"],"nt":"1","n":["EIf-EN' + + 'w7PrM52w4H-S7NGU2qVIfraXVIlV9hEAaMHg7W"],"bt":"0","b":[],"c":[],"a":[]}' + ); + + // (b'\x83B~\x04\x94\xe3\xceUQy\x11f\x0c\x93]\x1e\xbf\xacQ\xb5\xd6Y^\xa2E\xfa\x015\x98Y\xdd\xe8') + seed1 = new Uint8Array([ + 131, 66, 126, 4, 148, 227, 206, 85, 81, 121, 17, 102, 12, 147, 93, + 30, 191, 172, 81, 181, 214, 89, 94, 162, 69, 250, 1, 53, 152, 89, + 221, 232, + ]); + signer1 = new Signer({ raw: seed1 }); // next signing keypair transferable is default + assert.equal(signer1.code, MtrDex.Ed25519_Seed); + assert.equal(signer1.verfer.code, MtrDex.Ed25519); + // compute nxt digest + nxt1 = [new Diger({}, signer1.verfer.qb64b).qb64]; // dfault sith is 1 + assert.deepStrictEqual(nxt1, [ + 'EIf-ENw7PrM52w4H-S7NGU2qVIfraXVIlV9hEAaMHg7W', + ]); + serder0 = incept({ + keys: keys0, + ndigs: nxt1, + code: MtrDex.Blake3_256, + intive: true, + }); // intive true + assert.equal(serder0.sad['t'], Ilks.icp); + assert.equal( + serder0.sad['d'], + 'EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL' + ); + assert.equal(serder0.sad['d'], serder0.sad['i']); + assert.equal(serder0.sad['s'], '0'); + assert.equal(serder0.sad['kt'], 1); + assert.equal(serder0.sad['nt'], 1); + assert.deepStrictEqual(serder0.sad['n'], nxt1); + assert.equal(serder0.sad['bt'], 0); + assert.equal( + serder0.raw, + '{"v":"KERI10JSON000125_","t":"icp","d":"EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5u' + + 'k-WxvhsL","i":"EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL","s":"0","kt":1,' + + '"k":["DFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH"],"nt":1,"n":["EIf-ENw7Pr' + + 'M52w4H-S7NGU2qVIfraXVIlV9hEAaMHg7W"],"bt":0,"b":[],"c":[],"a":[]}' + ); + + const siger = signer0.sign(b(serder0.raw), 0) as Siger; + assert.equal( + siger.qb64, + 'AABB3MJGmBXxSEryNHw3YwZZLRl_6Ws4Me2WFq8PrQ6WlluSOpPqbwXuiG9RvNWZkqeW8A_0VRjokGMVRZ3m-c0I' + ); + + const msg = messagize(serder0, [siger]); + assert.equal( + d(msg), + '{"v":"KERI10JSON000125_","t":"icp","d":"EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL","i"' + + ':"EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL","s":"0","kt":1,"k":["DFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH"],' + + '"nt":1,"n":["EIf-ENw7PrM52w4H-S7NGU2qVIfraXVIlV9hEAaMHg7W"],"bt":0,"b":[],"c":[],"a":[]}' + + '-AABAABB3MJGmBXxSEryNHw3YwZZLRl_6Ws4Me2WFq8PrQ6WlluSOpPqbwXuiG9RvNWZkqeW8A_0VRjokGMVRZ3m-c0I' + ); + const seal = [ + 'SealEvent', + { + i: 'EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL', + s: '0', + d: 'EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL', + }, + ]; + const msgseal = messagize(serder0, [siger], seal); + assert.equal( + d(msgseal), + '{"v":"KERI10JSON000125_","t":"icp","d":"EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL","i"' + + ':"EIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL","s":"0","kt":1,"k":["DFs8BBx86uytIM0D2BhsE5rrqVIT8ef8mflpNceHo4XH"]' + + ',"nt":1,"n":["EIf-ENw7PrM52w4H-S7NGU2qVIfraXVIlV9hEAaMHg7W"],"bt":0,"b":[],"c":[],"a"' + + ':[]}-FABEIflL4H4134zYoRM6ls6Q086RLC_BhfNFh5uk-WxvhsL0AAAAAAAAAAAAAAAAAAAAAAAEIflL4H4134zYoRM6ls6Q086RLC_' + + 'BhfNFh5uk-WxvhsL-AABAABB3MJGmBXxSEryNHw3YwZZLRl_6Ws4Me2WFq8PrQ6WlluSOpPqbwXuiG9RvNWZkqeW8A_0VRjokGMVRZ3m-c0I' + ); + }); + + it('Rotate should create rotation event with hex sequence number', async () => { + await libsodium.ready; + + const signer0 = new Signer({ transferable: true }); + const signer1 = new Signer({ transferable: true }); + const keys0 = [signer0.verfer.qb64]; + const ndigs = [new Diger({}, signer1.verfer.qb64b).qb64]; + const serder = incept({ keys: keys0, ndigs }); + + function createRotation(sn: number) { + return rotate({ + keys: keys0, + pre: serder.sad.i, + ndigs: serder.sad.n, + sn, + isith: 1, + nsith: 1, + }).sad['s']; + } + + assert.equal(createRotation(1), '1'); + assert.equal(createRotation(10), 'a'); + assert.equal(createRotation(14), 'e'); + assert.equal(createRotation(255), 'ff'); + }); +}); diff --git a/packages/signify-ts/test/core/httping.test.ts b/packages/signify-ts/test/core/httping.test.ts new file mode 100644 index 00000000..1223f82b --- /dev/null +++ b/packages/signify-ts/test/core/httping.test.ts @@ -0,0 +1,87 @@ +import { assert, describe, it, vitest } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Salter } from '../../src/keri/core/salter.ts'; +import { b } from '../../src/keri/core/core.ts'; +import { + siginput, + desiginput, + SiginputArgs, +} from '../../src/keri/core/httping.ts'; +import * as utilApi from '../../src/keri/core/utils.ts'; + +describe('siginput', () => { + it('create valid Signature-Input header with signature', async () => { + await libsodium.ready; + const salt = '0123456789abcdef'; + const salter = new Salter({ raw: b(salt) }); + const signer = salter.signer(); + + const headers: Headers = new Headers([ + ['Content-Type', 'application/json'], + ['Content-Length', '256'], + ['Connection', 'close'], + [ + 'Signify-Resource', + 'EWJkQCFvKuyxZi582yJPb0wcwuW3VXmFNuvbQuBpgmIs', + ], + ['Signify-Timestamp', '2022-09-24T00:05:48.196795+00:00'], + ]); + vitest + .spyOn(utilApi, 'nowUTC') + .mockReturnValue(new Date('2021-01-01T00:00:00.000000+00:00')); + + const [header, sig] = siginput(signer, { + name: 'sig0', + method: 'POST', + path: '/signify', + headers, + fields: [ + 'Signify-Resource', + '@method', + '@path', + 'Signify-Timestamp', + ], + alg: 'ed25519', + keyid: signer.verfer.qb64, + } as SiginputArgs); + + assert.equal(header.size, 1); + assert.equal(header.has('Signature-Input'), true); + const sigipt = header.get('Signature-Input'); + assert.equal( + sigipt, + 'sig0=("Signify-Resource" "@method" "@path" "Signify-Timestamp");created=1609459200;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"' + ); + assert.equal( + sig.qb64, + '0BAJWoDvZXYKnq_9rFTy_mucctxk3rVK6szopNi1rq5WQcJSNIw-_PocSQNoQGD1Ow_s2mDI5-Qqm34Y56gUKQcF' + ); + }); +}); + +describe('desiginput', () => { + it('create valid Signature-Input header with signature', async () => { + await libsodium.ready; + const siginput = + 'sig0=("signify-resource" "@method" "@path" "signify-timestamp");created=1609459200;keyid="EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3";alg="ed25519"'; + + const inputs = desiginput(siginput); + assert.equal(inputs.length, 1); + const input = inputs[0]; + assert.deepStrictEqual(input.fields, [ + 'signify-resource', + '@method', + '@path', + 'signify-timestamp', + ]); + assert.equal(input.created, 1609459200); + assert.equal(input.alg, 'ed25519'); + assert.equal( + input.keyid, + 'EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3' + ); + assert.equal(input.expires, undefined); + assert.equal(input.nonce, undefined); + assert.equal(input.context, undefined); + }); +}); diff --git a/packages/signify-ts/test/core/indexer.test.ts b/packages/signify-ts/test/core/indexer.test.ts new file mode 100644 index 00000000..72f183d6 --- /dev/null +++ b/packages/signify-ts/test/core/indexer.test.ts @@ -0,0 +1,383 @@ +import libsodium from 'libsodium-wrappers-sumo'; +import { assert, describe, it } from 'vitest'; +import { IdrDex, Indexer } from '../../src/keri/core/indexer.ts'; +import { b, intToB64 } from '../../src/keri/core/core.ts'; +import { + decodeBase64Url, + encodeBase64Url, +} from '../../src/keri/core/base64.ts'; + +describe('Indexer', () => { + it('should encode and decode dual indexed signatures', async () => { + await libsodium.ready; + + assert.equal(Indexer.Sizes.get('A')!.hs, 1); // hard size + assert.equal(Indexer.Sizes.get('A')!.ss, 1); // soft size + assert.equal(Indexer.Sizes.get('A')!.os, 0); // other size + assert.equal(Indexer.Sizes.get('A')!.fs, 88); // full size + assert.equal(Indexer.Sizes.get('A')!.ls, 0); // lead size + + Indexer.Sizes.forEach((_, key) => { + assert.equal( + Indexer.Hards.get(key[0]), + Indexer.Sizes.get(key)!.hs, + `${key} hs not set` + ); + }); + + Indexer.Sizes.forEach((val, key) => { + assert.equal(val.hs > 0, true, `${key} hs incorrect`); + assert.equal(val.ss > 0, true); + assert.equal(val.os >= 0, true); + if (val.os > 0) { + assert.equal( + val.os == Math.floor(val.ss / 2), + true, + `${key} os/ss incorrect` + ); + } + if (val.fs != undefined) { + assert.equal( + val.fs >= val.hs + val.ss, + true, + `${key} fs incorrect` + ); + assert.equal(val.fs % 4 == 0, true, `${key} fs mod incorrect`); + } + }); + + assert.throws(() => { + new Indexer({}); + }); + + // sig = (b"\x99\xd2<9$$0\x9fk\xfb\x18\xa0\x8c@r\x122.k\xb2\xc7\x1fp\x0e'm\x8f@" + // b'\xaa\xa5\x8c\xc8n\x85\xc8!\xf6q\x91p\xa9\xec\xcf\x92\xaf)\xde\xca' + // b'\xfc\x7f~\xd7o|\x17\x82\x1d\xd4 { + new Indexer({ raw: shortsig }); + }); + + indexer = new Indexer({ qb64b: qsig64b }); + assert.equal(indexer.code, IdrDex.Ed25519_Sig); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.index, 0); + assert.equal(indexer.ondex, 0); + assert.deepStrictEqual(indexer.qb64b, qsig64b); + assert.deepStrictEqual(indexer.qb64, qsig64); + + indexer = new Indexer({ qb64: qsig64 }); + assert.equal(indexer.code, IdrDex.Ed25519_Sig); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.index, 0); + assert.equal(indexer.ondex, 0); + assert.deepStrictEqual(indexer.qb64b, qsig64b); + assert.deepStrictEqual(indexer.qb64, qsig64); + + const badq64sig2 = + 'AA_Z0jw5JCQwn2v7GKCMQHISMi5rsscfcA4nbY9AqqWMyG6FyCH2cZFwqezPkq8p3sr8f37Xb3wXgh3UPG8igSYJ'; + assert.throws(() => { + new Indexer({ qb64: badq64sig2 }); + }); + + const longqsig64 = qsig64 + 'ABCD'; + indexer = new Indexer({ qb64: longqsig64 }); + assert.equal(indexer.qb64.length, Indexer.Sizes.get(indexer.code)!.fs); + + const shortqsig64 = qsig64.slice(0, -4); + assert.throws(() => { + new Indexer({ qb64: shortqsig64 }); + }); + + qsig64 = + 'AFCZ0jw5JCQwn2v7GKCMQHISMi5rsscfcA4nbY9AqqWMyG6FyCH2cZFwqezPkq8p3sr8f37Xb3wXgh3UPG8igSYJ'; + qsig64b = b(qsig64); + qsig2b = decodeBase64Url(qsig64); + assert.equal(qsig2b.length, 66); + + indexer = new Indexer({ raw: sig, code: IdrDex.Ed25519_Sig, index: 5 }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Sig); + assert.equal(indexer.index, 5); + assert.equal(indexer.ondex, 5); + assert.equal(indexer.qb64, qsig64); + assert.deepStrictEqual(indexer.qb64b, qsig64b); + indexer._exfil(qsig64); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Sig); + assert.equal(indexer.qb64, qsig64); + assert.deepStrictEqual(indexer.qb64b, qsig64b); + + indexer = new Indexer({ + raw: sig, + code: IdrDex.Ed25519_Sig, + index: 5, + ondex: 5, + }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Sig); + assert.equal(indexer.index, 5); + assert.equal(indexer.ondex, 5); + assert.equal(indexer.qb64, qsig64); + assert.deepStrictEqual(indexer.qb64b, qsig64b); + + assert.throws(() => { + new Indexer({ + raw: sig, + code: IdrDex.Ed25519_Sig, + index: 5, + ondex: 0, + }); + }); + + assert.throws(() => { + new Indexer({ + raw: sig, + code: IdrDex.Ed25519_Sig, + index: 5, + ondex: 64, + }); + }); + + indexer = new Indexer({ raw: sig }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Sig); + assert.equal(indexer.index, 0); + assert.equal(indexer.ondex, 0); + assert.notEqual(indexer.qb64, qsig64); + assert.notDeepEqual(indexer.qb64b, qsig64b); + + indexer = new Indexer({ qb64: qsig64 }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Sig); + assert.equal(indexer.index, 5); + assert.equal(indexer.ondex, 5); + assert.equal(indexer.qb64, qsig64); + assert.deepStrictEqual(indexer.qb64b, qsig64b); + + // test index too big + assert.throws(() => { + new Indexer({ raw: sig, code: IdrDex.Ed25519_Sig, index: 65 }); + }); + + // test negative index + assert.throws(() => { + new Indexer({ raw: sig, code: IdrDex.Ed25519_Sig, index: -1 }); + }); + + // test non integer index + assert.throws(() => { + new Indexer({ raw: sig, code: IdrDex.Ed25519_Sig, index: 3.5 }); + }); + + let index = 67; + let qb64 = + '2ABDBDCZ0jw5JCQwn2v7GKCMQHISMi5rsscfcA4nbY9AqqWMyG6FyCH2cZFwqezPkq8p3sr8f37Xb3wXgh3UPG8igSYJ'; + let qb64b = b(qb64); + + indexer = new Indexer({ + raw: sig, + code: IdrDex.Ed25519_Big_Sig, + index: index, + }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Big_Sig); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, index); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + indexer = new Indexer({ + raw: sig, + code: IdrDex.Ed25519_Big_Sig, + index: index, + ondex: index, + }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Big_Sig); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, index); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + indexer = new Indexer({ qb64: qb64 }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Big_Sig); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, index); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + index = 90; + const ondex = 65; + qb64 = + '2ABaBBCZ0jw5JCQwn2v7GKCMQHISMi5rsscfcA4nbY9AqqWMyG6FyCH2cZFwqezPkq8p3sr8f37Xb3wXgh3UPG8igSYJ'; + qb64b = b(qb64); + + indexer = new Indexer({ + raw: sig, + code: IdrDex.Ed25519_Big_Sig, + index: index, + ondex: ondex, + }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Big_Sig); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, ondex); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + indexer = new Indexer({ qb64: qb64 }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, IdrDex.Ed25519_Big_Sig); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, ondex); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + index = 3; + let code = IdrDex.Ed25519_Crt_Sig; + qb64 = + 'BDCZ0jw5JCQwn2v7GKCMQHISMi5rsscfcA4nbY9AqqWMyG6FyCH2cZFwqezPkq8p3sr8f37Xb3wXgh3UPG8igSYJ'; + qb64b = b(qb64); + + indexer = new Indexer({ raw: sig, code: code, index: index }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, code); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, undefined); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + indexer = new Indexer({ qb64: qb64 }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, code); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, undefined); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + assert.throws(() => { + new Indexer({ raw: sig, code: code, index: index, ondex: index }); + }); + + // test negative index + assert.throws(() => { + new Indexer({ + raw: sig, + code: code, + index: index, + ondex: index + 2, + }); + }); + + index = 68; + code = IdrDex.Ed25519_Big_Crt_Sig; + qb64 = + '2BBEAACZ0jw5JCQwn2v7GKCMQHISMi5rsscfcA4nbY9AqqWMyG6FyCH2cZFwqezPkq8p3sr8f37Xb3wXgh3UPG8igSYJ'; + qb64b = b(qb64); + + indexer = new Indexer({ raw: sig, code: code, index: index }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, code); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, undefined); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + indexer = new Indexer({ qb64: qb64 }); + assert.deepStrictEqual(indexer.raw, sig); + assert.equal(indexer.code, code); + assert.equal(indexer.index, index); + assert.equal(indexer.ondex, undefined); + assert.equal(indexer.qb64, qb64); + assert.deepStrictEqual(indexer.qb64b, qb64b); + + assert.throws(() => { + new Indexer({ raw: sig, code: code, index: index, ondex: index }); + }); + + // test negative index + assert.throws(() => { + new Indexer({ + raw: sig, + code: code, + index: index, + ondex: index + 2, + }); + }); + }); +}); diff --git a/packages/signify-ts/test/core/manager.test.ts b/packages/signify-ts/test/core/manager.test.ts new file mode 100644 index 00000000..ee6a4b2e --- /dev/null +++ b/packages/signify-ts/test/core/manager.test.ts @@ -0,0 +1,893 @@ +import libsodium from 'libsodium-wrappers-sumo'; +import { + Algos, + Creatory, + Manager, + RandyCreator, + riKey, + SaltyCreator, +} from '../../src/keri/core/manager.ts'; +import { assert, describe, it, expect, vitest, Mocked } from 'vitest'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import { Salter, Tier } from '../../src/keri/core/salter.ts'; +import { Signer } from '../../src/keri/core/signer.ts'; +import { Encrypter } from '../../src/keri/core/encrypter.ts'; +import { Decrypter } from '../../src/keri/core/decrypter.ts'; +import { Cipher } from '../../src/keri/core/cipher.ts'; +import { Verfer } from '../../src/keri/core/verfer.ts'; +import { Diger } from '../../src/keri/core/diger.ts'; +import { Siger } from '../../src/keri/core/siger.ts'; +import { b } from '../../src/keri/core/core.ts'; +import { Cigar } from '../../src/keri/core/cigar.ts'; +import { + IdentifierManager, + IdentifierManagerParams, + IdentifierManagerFactory, + Prefixer, + RandyIdentifierManager, +} from '../../src/index.ts'; +import { + RandyKeyState, + KeyState, + HabState, +} from '../../src/keri/core/keyState.ts'; +import { randomUUID } from 'node:crypto'; + +describe('RandyCreator', () => { + it('should create sets of random signers', async () => { + await libsodium.ready; + + const randy = new RandyCreator(); + + // test default arguments + let keys = randy.create(); + assert.equal(keys.signers.length, 1); + assert.equal(keys.signers[0].qb64.length, 44); + assert.equal(keys.signers[0].code, MtrDex.Ed25519_Seed); + assert.equal(keys.signers[0].transferable, true); + + // Create 5 with default code + keys = randy.create(undefined, 5); + assert.equal(keys.signers.length, 5); + keys.signers.forEach((signer) => { + assert.equal(signer.qb64.length, 44); + assert.equal(signer.code, MtrDex.Ed25519_Seed); + assert.equal(signer.transferable, true); + }); + + // Create 3 with specified codes (the only one we support) + keys = randy.create([ + MtrDex.Ed25519_Seed, + MtrDex.Ed25519_Seed, + MtrDex.Ed25519_Seed, + ]); + assert.equal(keys.signers.length, 3); + keys.signers.forEach((signer) => { + assert.equal(signer.qb64.length, 44); + assert.equal(signer.code, MtrDex.Ed25519_Seed); + assert.equal(signer.transferable, true); + }); + }); +}); + +describe('SaltyCreator', () => { + it('should create sets of salty signers', async () => { + await libsodium.ready; + + // Test default arguments + let salty = new SaltyCreator(); + assert.equal(salty.salter.code, MtrDex.Salt_128); + assert.equal(salty.salt, salty.salter.qb64); + assert.equal(salty.stem, ''); + assert.equal(salty.tier, salty.salter.tier); + + let keys = salty.create(); + assert.equal(keys.signers.length, 1); + assert.equal(keys.signers[0].qb64.length, 44); + assert.equal(keys.signers[0].code, MtrDex.Ed25519_Seed); + assert.equal(keys.signers[0].transferable, true); + + keys = salty.create(undefined, 2, MtrDex.Ed25519_Seed, false); + assert.equal(keys.signers.length, 2); + keys.signers.forEach((signer) => { + assert.equal(signer.code, MtrDex.Ed25519_Seed); + assert.equal(signer.verfer.code, MtrDex.Ed25519N); + assert.equal(signer.qb64.length, 44); + }); + + const raw = '0123456789abcdef'; + const salter = new Salter({ raw: b(raw) }); + const salt = salter.qb64; + assert.equal(salter.qb64, '0AAwMTIzNDU2Nzg5YWJjZGVm'); + salty = new SaltyCreator(salter.qb64); + assert.equal(salty.salter.code, MtrDex.Salt_128); + assert.deepStrictEqual(salty.salter.raw, b(raw)); + assert.equal(salty.salter.qb64, salt); + assert.equal(salty.salt, salty.salter.qb64); + assert.equal(salty.stem, ''); + assert.equal(salty.tier, salty.salter.tier); + keys = salty.create(); + assert.equal(keys.signers.length, 1); + assert.equal(keys.signers[0].code, MtrDex.Ed25519_Seed); + assert.equal( + keys.signers[0].qb64, + 'AO0hmkIVsjCoJY1oUe3-QqHlMBVIhFX1tQfN_8SPKiNF' + ); + assert.equal(keys.signers[0].verfer.code, MtrDex.Ed25519); + assert.equal( + keys.signers[0].verfer.qb64, + 'DHHneREQ1eZyQNc5nEsQYx1FqFVL1OTXmvmatTE77Cfe' + ); + + keys = salty.create( + undefined, + 1, + MtrDex.Ed25519_Seed, + false, + 0, + 0, + 0, + true + ); + assert.equal(keys.signers.length, 1); + assert.equal(keys.signers[0].code, MtrDex.Ed25519_Seed); + assert.equal( + keys.signers[0].qb64, + 'AOVkNmL_dZ5pjvp-_nS5EJbs0xe32MODcOUOym-0aCBL' + ); + assert.equal(keys.signers[0].verfer.code, MtrDex.Ed25519N); + assert.equal( + keys.signers[0].verfer.qb64, + 'BB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI' + ); + }); +}); + +describe('Creator', () => { + it('should create Randy or Salty creator', async () => { + await libsodium.ready; + + const raw = '0123456789abcdef'; + const salter = new Salter({ raw: b(raw) }); + const salt = salter.qb64; + + let creator = new Creatory(Algos.salty).make(salt) as SaltyCreator; + assert.equal(creator instanceof SaltyCreator, true); + assert.equal(creator.salter.qb64, salt); + + creator = new Creatory().make(salt) as SaltyCreator; + assert.equal(creator instanceof SaltyCreator, true); + assert.equal(creator.salter.qb64, salt); + + const rcreator = new Creatory(Algos.randy).make(salt) as RandyCreator; + assert.equal(rcreator instanceof RandyCreator, true); + }); +}); + +const ser = + '{"vs":"KERI10JSON0000fb_","pre":"EvEnZMhz52iTrJU8qKwtDxzmypyosgG' + + '70m6LIjkiCdoI","sn":"0","ilk":"icp","sith":"1","keys":["DSuhyBcP' + + 'ZEZLK-fcw5tzHn2N46wRCG_ZOoeKtWTOunRA"],"nxt":"EPYuj8mq_PYYsoBKkz' + + 'X1kxSPGYBWaIya3slgCOyOtlqU","toad":"0","wits":[],"cnfg":[]}-AABA' + + 'ApYcYd1cppVg7Inh2YCslWKhUwh59TrPpIoqWxN2A38NCbTljvmBPBjSGIFDBNOv' + + 'VjHpdZlty3Hgk6ilF8pVpAQ'; + +const keystate: KeyState = { + s: '0', + d: 'a digest', + i: '', + p: '', + f: '', + dt: '', + et: '', + kt: '', + k: [], + nt: '', + n: [], + bt: '', + b: [], + c: [], + ee: { + s: '', + d: '', + br: [], + ba: [], + }, + di: '', +}; + +describe('Manager', () => { + it('should manage key pairs for identifiers', async () => { + await libsodium.ready; + + const raw = '0123456789abcdef'; + const salter = new Salter({ raw: b(raw) }); + const salt = salter.qb64; + assert.equal(salt, '0AAwMTIzNDU2Nzg5YWJjZGVm'); + const stem = 'red'; + + // Create a randy Manager without encryption should raise an exception + assert.throws(() => { + new Manager({ algo: Algos.randy }); + }); + + // cryptseed0 = b('h,#|\x8ap"\x12\xc43t2\xa6\xe1\x18\x19\xf0f2,y\xc4\xc21@\xf5@\x15.\xa2\x1a\xcf') + const cryptseed0 = new Uint8Array([ + 104, 44, 35, 124, 138, 112, 34, 18, 196, 51, 116, 50, 166, 225, 24, + 25, 240, 102, 50, 44, 121, 196, 194, 49, 64, 245, 64, 21, 46, 162, + 26, 207, + ]); + const cryptsigner0 = new Signer({ + raw: cryptseed0, + code: MtrDex.Ed25519_Seed, + transferable: false, + }); + const seed0 = cryptsigner0.qb64; + const seed0b = cryptsigner0.qb64b; + const aeid0 = cryptsigner0.verfer.qb64; + assert.equal(aeid0, 'BCa7mK96FwxkU0TdF54Yqg3qBDXUWpOhQ_Mtr7E77yZB'); + const decrypter0 = new Decrypter({}, seed0b); + const encrypter0 = new Encrypter({}, b(aeid0)); + assert.equal(encrypter0.verifySeed(seed0b), true); + + // cryptseed1 = (b"\x89\xfe{\xd9'\xa7\xb3\x89#\x19\xbec\xee\xed\xc0\xf9\x97\xd0\x8f9\x1dyNI" + // b'I\x98\xbd\xa4\xf6\xfe\xbb\x03') + const cryptseed1 = new Uint8Array([ + 137, 254, 123, 217, 39, 167, 179, 137, 35, 25, 190, 99, 238, 237, + 192, 249, 151, 208, 143, 57, 29, 121, 78, 73, 73, 152, 189, 164, + 246, 254, 187, 3, + ]); + const cryptsigner1 = new Signer({ + raw: cryptseed1, + code: MtrDex.Ed25519_Seed, + transferable: false, + }); + const seed1 = cryptsigner1.qb64b; + const aeid1 = cryptsigner1.verfer.qb64; + assert.equal(aeid1, 'BEcOrMrG_7r_NWaLl6h8UJapwIfQWIkjrIPXkCZm2fFM'); + // let decrypter1 = new Decrypter({}, seed1) + const encrypter1 = new Encrypter({}, b(aeid1)); + assert.equal(encrypter1.verifySeed(seed1), true); + + const manager = new Manager({ + seed: seed0, + salter: salter, + aeid: aeid0, + }); + assert.equal(manager.encrypter!.qb64, encrypter0.qb64); + assert.equal(manager.decrypter!.qb64, decrypter0.qb64); + assert.equal(manager.seed, seed0); + assert.equal(manager.aeid, aeid0); + + assert.equal(manager.algo, Algos.salty); + assert.equal(manager.salt, salt); + assert.equal(manager.pidx, 0); + assert.equal(manager.tier, Tier.low); + const saltCipher0 = new Cipher({ qb64: manager.ks.getGbls('salt') }); + assert.equal(saltCipher0.decrypt(undefined, seed0b).qb64, salt); + + let [verfers, digers] = manager.incept({ salt: salt, temp: true }); + assert.equal(verfers.length, 1); + assert.equal(digers.length, 1); + assert.equal(manager.pidx, 1); + + let spre = verfers[0].qb64; + assert.equal(spre, 'DB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI'); + + let pp = manager.ks.getPrms(spre)!; + assert.equal(pp.pidx, 0); + assert.equal(pp.algo, Algos.salty); + assert.equal(manager.decrypter!.decrypt(b(pp.salt)).qb64, salt); + assert.equal(pp.stem, ''); + assert.equal(pp.tier, Tier.low); + + let ps = manager.ks.getSits(spre)!; + assert.deepStrictEqual(ps.old.pubs, []); + assert.equal(ps.new.pubs.length, 1); + assert.deepStrictEqual(ps.new.pubs, [ + 'DB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI', + ]); + assert.equal(ps.new.ridx, 0); + assert.equal(ps.new.kidx, 0); + assert.equal(ps.nxt.pubs.length, 1); + assert.deepStrictEqual(ps.nxt.pubs, [ + 'DB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI', + ]); + assert.equal(ps.nxt.ridx, 1); + assert.equal(ps.nxt.kidx, 1); + + let keys = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + assert.deepStrictEqual(keys, ps.new.pubs); + + let pl = manager.ks.getPubs(riKey(spre, ps.new.ridx))!; + assert.deepStrictEqual(pl.pubs, ps.new.pubs); + pl = manager.ks.getPubs(riKey(spre, ps.nxt.ridx))!; + assert.deepStrictEqual(pl.pubs, ps.nxt.pubs); + + let digs = Array.from(digers, (diger: Diger) => diger.qb64); + assert.deepStrictEqual(digs, [ + 'ENmcKrctbztF36MttN7seUYJqH2IMnkavBgGLR6Mj2-B', + ]); + + const oldspre = spre; + spre = 'DCu5o5cxzv1lgMqxMVG3IcCNK4lpFfpMM-9rfkY3XVUc'; + manager.move(oldspre, spre); + + pl = manager.ks.getPubs(riKey(spre, ps.new.ridx))!; + assert.deepStrictEqual(pl.pubs, ps.new.pubs); + pl = manager.ks.getPubs(riKey(spre, ps.nxt.ridx))!; + assert.deepStrictEqual(pl.pubs, ps.nxt.pubs); + + const serb = b(ser); + let psigers = manager.sign({ ser: serb, pubs: ps.new.pubs }); + assert.equal(psigers.length, 1); + assert.equal(psigers[0] instanceof Siger, true); + let vsigers = manager.sign({ ser: serb, verfers: verfers }); + let psigs = Array.from( + psigers as Array, + (psiger) => psiger.qb64 + ); + let vsigs = Array.from( + vsigers as Array, + (vsiger) => vsiger.qb64 + ); + assert.deepStrictEqual(psigs, vsigs); + assert.equal( + psigs[0], + 'AACRPqO6vdXm1oSSa82rmVVHikf7NdN4JXjOWEk30Ub5JHChL0bW6DzJfA-7VlgLm_B1XR0Z61FweP87bBQpVawI' + ); + + // Test sign with indices + const indices = [3]; + psigers = manager.sign({ + ser: serb, + pubs: ps.new.pubs, + indices: indices, + }) as Array; + assert.equal(psigers.length, 1); + assert.equal(psigers[0] instanceof Siger, true); + assert.equal(psigers[0].index, indices[0]); + psigs = Array.from(psigers as Array, (psiger) => psiger.qb64); + assert.equal( + psigs[0], + 'ADCRPqO6vdXm1oSSa82rmVVHikf7NdN4JXjOWEk30Ub5JHChL0bW6DzJfA-7VlgLm_B1XR0Z61FweP87bBQpVawI' + ); + + vsigers = manager.sign({ + ser: serb, + verfers: verfers, + indices: indices, + }) as Array; + assert.equal(vsigers.length, 1); + assert.equal(vsigers[0] instanceof Siger, true); + assert.equal(vsigers[0].index, indices[0]); + vsigs = Array.from(vsigers as Array, (vsiger) => vsiger.qb64); + assert.deepStrictEqual(psigs, vsigs); + + const pcigars = manager.sign({ + ser: serb, + pubs: ps.new.pubs, + indexed: false, + }); + assert.equal(pcigars.length, 1); + assert.equal(pcigars[0] instanceof Cigar, true); + const vcigars = manager.sign({ + ser: serb, + verfers: verfers, + indexed: false, + }); + assert.equal(vcigars.length, 1); + const pcigs = Array.from( + pcigars as Array, + (psiger) => psiger.qb64 + ); + const vcigs = Array.from( + vcigars as Array, + (vsiger) => vsiger.qb64 + ); + assert.deepStrictEqual(vcigs, pcigs); + assert.equal( + pcigs[0], + '0BCRPqO6vdXm1oSSa82rmVVHikf7NdN4JXjOWEk30Ub5JHChL0bW6DzJfA-7VlgLm_B1XR0Z61FweP87bBQpVawI' + ); + + let oldpubs = Array.from(verfers, (verfer) => verfer.qb64); + let hashes = manager.rotate({ pre: spre }); + verfers = hashes[0]; + digers = hashes[1]; + + assert.equal(verfers.length, 1); + assert.equal(digers.length, 1); + pp = manager.ks.getPrms(spre)!; + assert.equal(pp.pidx, 0); + assert.equal(pp.algo, Algos.salty); + assert.equal(manager.decrypter!.decrypt(b(pp.salt)).qb64, salt); + assert.equal(pp.stem, ''); + assert.equal(pp.tier, Tier.low); + + ps = manager.ks.getSits(spre)!; + assert.deepStrictEqual(ps.old.pubs, [ + 'DB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI', + ]); + assert.equal(ps.new.pubs.length, 1); + assert.deepStrictEqual(ps.new.pubs, [ + 'DB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI', + ]); + assert.equal(ps.new.ridx, 1); + assert.equal(ps.new.kidx, 1); + assert.equal(ps.nxt.pubs.length, 1); + assert.deepStrictEqual(ps.nxt.pubs, [ + 'DHHneREQ1eZyQNc5nEsQYx1FqFVL1OTXmvmatTE77Cfe', + ]); + assert.equal(ps.nxt.ridx, 2); + assert.equal(ps.nxt.kidx, 2); + + keys = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + assert.deepStrictEqual(keys, ps.new.pubs); + + digs = Array.from(digers, (diger: Diger) => diger.qb64); + assert.deepStrictEqual(digs, [ + 'ECl1Env_5PQHqVMpHgoqg9H9mT7ENtk0Q499cmMT6Fvz', + ]); + + assert.deepStrictEqual(oldpubs, ps.old.pubs); + + oldpubs = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + const deadpubs = ps.old.pubs; + + manager.rotate({ pre: spre }); + + pp = manager.ks.getPrms(spre)!; + assert.equal(pp.pidx, 0); + + ps = manager.ks.getSits(spre)!; + assert.deepStrictEqual(oldpubs, ps.old.pubs); + + deadpubs.forEach((pub: string) => { + assert.equal(manager.ks.getPris(pub, decrypter0), undefined); + }); + + pl = manager.ks.getPubs(riKey(spre, ps.new.ridx))!; + assert.deepStrictEqual(pl.pubs, ps.new.pubs); + + pl = manager.ks.getPubs(riKey(spre, ps.nxt.ridx))!; + assert.deepStrictEqual(pl.pubs, ps.nxt.pubs); + + hashes = manager.rotate({ pre: spre, ncount: 0 }); + digers = hashes[1]; + pp = manager.ks.getPrms(spre)!; + assert.equal(pp.pidx, 0); + + ps = manager.ks.getSits(spre)!; + assert.equal(ps.nxt.pubs.length, 0); + assert.equal(digers.length, 0); + + assert.throws(() => { + manager.rotate({ pre: spre }); + }); + + // randy algo support + hashes = manager.incept({ algo: Algos.randy }); + verfers = hashes[0]; + digers = hashes[1]; + + assert.equal(verfers.length, 1); + assert.equal(digers.length, 1); + assert.equal(manager.pidx, 2); + let rpre = verfers[0].qb64; + + pp = manager.ks.getPrms(rpre)!; + assert.equal(pp.pidx, 1); + assert.equal(pp.algo, Algos.randy); + assert.equal(pp.salt, ''); + assert.equal(pp.stem, ''); + assert.equal(pp.tier, ''); + + ps = manager.ks.getSits(rpre)!; + assert.deepStrictEqual(ps.old.pubs, []); + assert.equal(ps.new.pubs.length, 1); + assert.deepStrictEqual(ps.new.pubs.length, 1); + assert.equal(ps.new.ridx, 0); + assert.equal(ps.new.kidx, 0); + assert.equal(ps.nxt.pubs.length, 1); + assert.equal(ps.nxt.ridx, 1); + assert.equal(ps.nxt.kidx, 1); + + keys = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + keys.forEach((key) => { + assert.notEqual(manager.ks.getPris(key, decrypter0), undefined); + }); + + const oldrpre = rpre; + rpre = 'DMqxMVG3IcCNK4lpFfCu5o5cxzv1lgpMM-9rfkY3XVUc'; + manager.move(oldrpre, rpre); + + oldpubs = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + manager.rotate({ pre: rpre }); + + pp = manager.ks.getPrms(rpre)!; + assert.equal(pp.pidx, 1); + ps = manager.ks.getSits(rpre)!; + assert.deepStrictEqual(oldpubs, ps.old.pubs); + + // randy algo incept with null nxt + hashes = manager.incept({ algo: Algos.randy, ncount: 0 }); + verfers = hashes[0]; + digers = hashes[1]; + + assert.equal(manager.pidx, 3); + rpre = verfers[0].qb64; + + pp = manager.ks.getPrms(rpre)!; + assert.equal(pp.pidx, 2); + + ps = manager.ks.getSits(rpre)!; + assert.deepStrictEqual(ps.nxt.pubs, []); + assert.deepStrictEqual(digers, []); + + // attempt to rotate after null + assert.throws(() => { + manager.rotate({ pre: rpre }); + }); + + // salty algorithm incept with stem + hashes = manager.incept({ salt: salt, stem: stem, temp: true }); + verfers = hashes[0]; + digers = hashes[1]; + + assert.equal(verfers.length, 1); + assert.equal(digers.length, 1); + assert.equal(manager.pidx, 4); + + spre = verfers[0].qb64; + assert.equal(spre, 'DOtu4gX3oc4feusD8wWIykLhjkpiJHXEe29eJ2b_1CyM'); + + pp = manager.ks.getPrms(spre)!; + assert.equal(pp.pidx, 3); + assert.equal(pp.algo, Algos.salty); + assert.equal(manager.decrypter!.decrypt(b(pp.salt)).qb64, salt); + assert.equal(pp.stem, 'red'); + assert.equal(pp.tier, Tier.low); + + ps = manager.ks.getSits(spre)!; + assert.deepStrictEqual(ps.old.pubs, []); + assert.equal(ps.new.pubs.length, 1); + assert.deepStrictEqual(ps.new.pubs, [ + 'DOtu4gX3oc4feusD8wWIykLhjkpiJHXEe29eJ2b_1CyM', + ]); + assert.equal(ps.new.ridx, 0); + assert.equal(ps.new.kidx, 0); + assert.equal(ps.nxt.pubs.length, 1); + assert.deepStrictEqual(ps.nxt.pubs, [ + 'DBzZ6vejSNAZpXv1SDRnIF_P1UqcW5d2pu2U-v-uhXvE', + ]); + assert.equal(ps.nxt.ridx, 1); + assert.equal(ps.nxt.kidx, 1); + + keys = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + assert.deepStrictEqual(keys, ps.new.pubs); + + digs = Array.from(digers, (diger: Diger) => diger.qb64); + assert.deepStrictEqual(digs, [ + 'EIGjhyyBRcqCkPE9bmkph7morew0wW0ak-rQ-dHCH-M2', + ]); + + hashes = manager.incept({ + ncount: 0, + salt: salt, + stem: 'wit0', + transferable: false, + temp: true, + }); + verfers = hashes[0]; + digers = hashes[1]; + + const witpre0 = verfers[0].qb64; + assert.equal( + verfers[0].qb64, + 'BOTNI4RzN706NecNdqTlGEcMSTWiFUvesEqmxWR_op8n' + ); + assert.equal(verfers[0].code, MtrDex.Ed25519N); + assert.notEqual(digers, undefined); + + hashes = manager.incept({ + ncount: 0, + salt: salt, + stem: 'wit1', + transferable: false, + temp: true, + }); + verfers = hashes[0]; + digers = hashes[1]; + + const witpre1 = verfers[0].qb64; + assert.equal( + verfers[0].qb64, + 'BAB_5xNXH4hoxDCtAHPFPDedZ6YwTo8mbdw_v0AOHOMt' + ); + assert.equal(verfers[0].code, MtrDex.Ed25519N); + assert.notEqual(digers, undefined); + + assert.notEqual(witpre0, witpre1); + }); + + it('should support only Salty/Encrypted, Salty/Unencrypted and Randy/Encrypted', async () => { + await libsodium.ready; + + // Support Salty/Unencrypted - pass only stretched passcode as Salt. + const passcode = '0123456789abcdefghijk'; + const salter = new Salter({ raw: b(passcode) }); + const salt = salter.qb64; + + const manager = new Manager({ salter: salter }); + assert.equal(manager.encrypter, undefined); + + let [verfers, digers] = manager.incept({ salt: salt, temp: true }); + assert.equal(verfers.length, 1); + assert.equal(digers.length, 1); + assert.equal(manager.pidx, 1); + + let spre = verfers[0].qb64; + assert.equal(spre, 'DB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI'); + let ps = manager.ks.getSits(spre)!; + + let keys = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + assert.deepStrictEqual(keys, ps.new.pubs); + + let pl = manager.ks.getPubs(riKey(spre, ps.new.ridx))!; + assert.deepStrictEqual(pl.pubs, ps.new.pubs); + pl = manager.ks.getPubs(riKey(spre, ps.nxt.ridx))!; + assert.deepStrictEqual(pl.pubs, ps.nxt.pubs); + + let ppt = manager.ks.getPths(ps.new.pubs[0]); + assert.equal(ppt!.path, '0'); + assert.equal(ppt!.code, 'A'); + assert.equal(ppt!.tier, 'low'); + assert.equal(ppt!.temp, true); + + let digs = Array.from(digers, (diger: Diger) => diger.qb64); + assert.deepStrictEqual(digs, [ + 'ENmcKrctbztF36MttN7seUYJqH2IMnkavBgGLR6Mj2-B', + ]); + + const serb = b(ser); + let psigers = manager.sign({ ser: serb, pubs: ps.new.pubs }); + assert.equal(psigers.length, 1); + assert.equal(psigers[0] instanceof Siger, true); + let vsigers = manager.sign({ ser: serb, verfers: verfers }); + let psigs = Array.from( + psigers as Array, + (psiger) => psiger.qb64 + ); + let vsigs = Array.from( + vsigers as Array, + (vsiger) => vsiger.qb64 + ); + assert.deepStrictEqual(psigs, vsigs); + assert.equal( + psigs[0], + 'AACRPqO6vdXm1oSSa82rmVVHikf7NdN4JXjOWEk30Ub5JHChL0bW6DzJfA-7VlgLm_B1XR0Z61FweP87bBQpVawI' + ); + + const oldspre = spre; + spre = 'DCu5o5cxzv1lgMqxMVG3IcCNK4lpFfpMM-9rfkY3XVUc'; + manager.move(oldspre, spre); + + const oldpubs = Array.from(verfers, (verfer) => verfer.qb64); + const hashes = manager.rotate({ pre: spre }); + verfers = hashes[0]; + digers = hashes[1]; + + assert.equal(verfers.length, 1); + assert.equal(digers.length, 1); + const pp = manager.ks.getPrms(spre)!; + assert.equal(pp.pidx, 0); + assert.equal(pp.algo, Algos.salty); + assert.equal(pp.salt, ''); + assert.equal(pp.stem, ''); + assert.equal(pp.tier, Tier.low); + + ps = manager.ks.getSits(spre)!; + assert.deepStrictEqual(ps.old.pubs, [ + 'DB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI', + ]); + assert.equal(ps.new.pubs.length, 1); + assert.deepStrictEqual(ps.new.pubs, [ + 'DB-fH5uto5o5XHZjNN3_W3PdT4MIyTCmQWDzMxMZV2kI', + ]); + assert.equal(ps.new.ridx, 1); + assert.equal(ps.new.kidx, 1); + assert.equal(ps.nxt.pubs.length, 1); + assert.deepStrictEqual(ps.nxt.pubs, [ + 'DHHneREQ1eZyQNc5nEsQYx1FqFVL1OTXmvmatTE77Cfe', + ]); + assert.equal(ps.nxt.ridx, 2); + assert.equal(ps.nxt.kidx, 2); + + keys = Array.from(verfers, (verfer: Verfer) => verfer.qb64); + assert.deepStrictEqual(keys, ps.new.pubs); + + digs = Array.from(digers, (diger: Diger) => diger.qb64); + assert.deepStrictEqual(digs, [ + 'ECl1Env_5PQHqVMpHgoqg9H9mT7ENtk0Q499cmMT6Fvz', + ]); + + assert.deepStrictEqual(oldpubs, ps.old.pubs); + + ppt = manager.ks.getPths(ps.new.pubs[0]); + assert.equal(ppt!.path, '0'); + assert.equal(ppt!.code, 'A'); + assert.equal(ppt!.tier, 'low'); + assert.equal(ppt!.temp, true); + + psigers = manager.sign({ ser: serb, pubs: ps.new.pubs }); + assert.equal(psigers.length, 1); + assert.equal(psigers[0] instanceof Siger, true); + vsigers = manager.sign({ ser: serb, verfers: verfers }); + psigs = Array.from(psigers as Array, (psiger) => psiger.qb64); + vsigs = Array.from(vsigers as Array, (vsiger) => vsiger.qb64); + assert.deepStrictEqual(psigs, vsigs); + assert.equal( + psigs[0], + 'AACRPqO6vdXm1oSSa82rmVVHikf7NdN4JXjOWEk30Ub5JHChL0bW6DzJfA-7VlgLm_B1XR0Z61FweP87bBQpVawI' + ); + }); + + it('Should support creating and getting randy keeper', async () => { + const passcode = '0123456789abcdefghijk'; + const salter = new Salter({ raw: b(passcode) }); + + const manager = new IdentifierManagerFactory(salter, []); + + const keeper0 = manager.new( + Algos.randy, + 0, + {} + ) as RandyIdentifierManager; + const [keys] = await keeper0.incept(false); + const prefixes = new Prefixer({ qb64: keys[0] }); + + const keeper1 = manager.get({ + prefix: prefixes.qb64, + name: '', + state: keystate, + randy: keeper0.params() as RandyKeyState, + transferable: false, + windexes: [], + icp_dt: '2023-12-01T10:05:25.062609+00:00', + }); + + assert(keeper0 instanceof RandyIdentifierManager); + assert(keeper1 instanceof RandyIdentifierManager); + }); + + it('Should throw if algo is not supported', async () => { + const passcode = '0123456789abcdefghijk'; + const salter = new Salter({ raw: b(passcode) }); + + const manager = new IdentifierManagerFactory(salter, []); + + expect(() => manager.new(randomUUID() as Algos, 0, {})).toThrow( + 'Unknown algo' + ); + + // Just use a partial for testing purpose + const partialHabState: Partial = { + prefix: '', + name: '', + state: keystate, + transferable: false, + windexes: [], + icp_dt: '2023-12-01T10:05:25.062609+00:00', + }; + expect(() => manager.get(partialHabState as HabState)).toThrow( + 'No algo specified' + ); + }); + + describe('External Module ', () => { + class MockModule implements Mocked { + #params: Record; + + constructor( + public pidx: number, + params: IdentifierManagerParams + ) { + this.#params = params; + } + + signers: Signer[] = []; + sign = vitest.fn(); + algo: Algos = Algos.extern; + incept = vitest.fn(); + rotate = vitest.fn(); + params = vitest.fn(() => this.#params); + } + + it('Should support creating external keeper module', async () => { + const passcode = '0123456789abcdefghijk'; + const salter = new Salter({ raw: b(passcode) }); + + const manager = new IdentifierManagerFactory(salter, [ + { module: MockModule, name: 'mock', type: 'mock' }, + ]); + + const param = randomUUID(); + const keeper = manager.new(Algos.extern, 0, { + extern_type: 'mock', + param, + }); + + assert(keeper instanceof MockModule); + expect(keeper.params()).toMatchObject({ param }); + }); + + it('Should throw if external keeper module is not addede', async () => { + const passcode = '0123456789abcdefghijk'; + const salter = new Salter({ raw: b(passcode) }); + + const manager = new IdentifierManagerFactory(salter, []); + + const param = randomUUID(); + expect(() => + manager.new(Algos.extern, 0, { + extern_type: 'mock', + param, + }) + ).toThrow('unsupported external module type mock'); + }); + + it('Should support getting external keeper module', async () => { + const passcode = '0123456789abcdefghijk'; + const salter = new Salter({ raw: b(passcode) }); + + const manager = new IdentifierManagerFactory(salter, [ + { module: MockModule, name: 'mock', type: 'mock' }, + ]); + + const param = randomUUID(); + + const keeper = manager.get({ + name: randomUUID(), + prefix: '', + state: keystate, + windexes: [], + extern: { + extern_type: 'mock', + pidx: 3, + param, + }, + transferable: true, + icp_dt: '2023-12-01T10:05:25.062609+00:00', + }); + + assert(keeper instanceof MockModule); + expect(keeper.params()).toMatchObject({ param, pidx: 3 }); + }); + + it('Should throw when trying to get external keeper that is not registered', async () => { + const passcode = '0123456789abcdefghijk'; + const salter = new Salter({ raw: b(passcode) }); + + const manager = new IdentifierManagerFactory(salter, []); + + const param = randomUUID(); + + expect(() => + manager.get({ + name: randomUUID(), + prefix: '', + state: keystate, + windexes: [], + extern: { + extern_type: 'mock', + pidx: 3, + param, + }, + transferable: true, + icp_dt: '2023-12-01T10:05:25.062609+00:00', + }) + ).toThrow('unsupported external module type mock'); + }); + }); +}); diff --git a/packages/signify-ts/test/core/matter.test.ts b/packages/signify-ts/test/core/matter.test.ts new file mode 100644 index 00000000..d346da5d --- /dev/null +++ b/packages/signify-ts/test/core/matter.test.ts @@ -0,0 +1,13 @@ +import { assert, describe, it } from 'vitest'; + +import { Sizage } from '../../src/keri/core/matter.ts'; + +describe('Sizage', () => { + it('should hold size values in 4 properties', async () => { + const sizage = new Sizage(1, 2, 3, 4); + assert.equal(sizage.hs, 1); + assert.equal(sizage.ss, 2); + assert.equal(sizage.fs, 3); + assert.equal(sizage.ls, 4); + }); +}); diff --git a/packages/signify-ts/test/core/number.test.ts b/packages/signify-ts/test/core/number.test.ts new file mode 100644 index 00000000..459cc501 --- /dev/null +++ b/packages/signify-ts/test/core/number.test.ts @@ -0,0 +1,33 @@ +import { assert, describe, it } from 'vitest'; +import { CesrNumber } from '../../src/keri/core/number.ts'; + +describe('THolder', () => { + it('should hold thresholds', async () => { + let n = new CesrNumber({}, undefined, '0'); + assert.equal(n.num, 0); + assert.equal(n.numh, '0'); + n = new CesrNumber({}, 0); + assert.equal(n.num, 0); + assert.equal(n.numh, '0'); + + n = new CesrNumber({}, 1); + assert.equal(n.num, 1); + assert.equal(n.numh, '1'); + + n = new CesrNumber({}, 15); + assert.equal(n.num, 15); + assert.equal(n.numh, 'f'); + + n = new CesrNumber({}, undefined, '1'); + assert.equal(n.num, 1); + assert.equal(n.numh, '1'); + + n = new CesrNumber({}, undefined, 'f'); + assert.equal(n.num, 15); + assert.equal(n.numh, 'f'); + + n = new CesrNumber({}, undefined, '15'); + assert.equal(n.num, 21); + assert.equal(n.numh, '15'); + }); +}); diff --git a/packages/signify-ts/test/core/pather.test.ts b/packages/signify-ts/test/core/pather.test.ts new file mode 100644 index 00000000..69749d3e --- /dev/null +++ b/packages/signify-ts/test/core/pather.test.ts @@ -0,0 +1,53 @@ +import { assert, describe, it } from 'vitest'; +import { Pather } from '../../src/keri/core/pather.ts'; +import { b } from '../../src/index.ts'; + +describe('Pather', () => { + it('should path-ify stuff (and back again)', () => { + assert.throws(() => { + new Pather({}); + }); + + let path: string[] = []; + let pather = new Pather({}, undefined, path); + assert.equal(pather.bext, '-'); + assert.equal(pather.qb64, '6AABAAA-'); + assert.deepStrictEqual(pather.raw, b('>')); + assert.deepStrictEqual(pather.path, path); + + path = ['a', 'b', 'c']; + pather = new Pather({}, undefined, path); + assert.equal(pather.bext, '-a-b-c'); + assert.equal(pather.qb64, '5AACAA-a-b-c'); + assert.deepStrictEqual( + pather.raw, + new Uint8Array([15, 154, 249, 191, 156]) + ); + assert.deepStrictEqual(pather.path, path); + + path = ['0', '1', '2']; + pather = new Pather({}, undefined, path); + assert.equal(pather.bext, '-0-1-2'); + assert.equal(pather.qb64, '5AACAA-0-1-2'); + assert.deepStrictEqual( + pather.raw, + new Uint8Array([15, 180, 251, 95, 182]) + ); + assert.deepStrictEqual(pather.path, path); + + path = ['field0', '1', '0']; + pather = new Pather({}, undefined, path); + assert.equal(pather.bext, '-field0-1-0'); + assert.equal(pather.qb64, '4AADA-field0-1-0'); + assert.deepStrictEqual( + pather.raw, + new Uint8Array([3, 231, 226, 122, 87, 116, 251, 95, 180]) + ); + assert.deepStrictEqual(pather.path, path); + + path = ['Not$Base64', '@moreso', '*again']; + assert.throws(() => { + new Pather({}, undefined, path); + }); + }); +}); diff --git a/packages/signify-ts/test/core/prefixer.test.ts b/packages/signify-ts/test/core/prefixer.test.ts new file mode 100644 index 00000000..28694433 --- /dev/null +++ b/packages/signify-ts/test/core/prefixer.test.ts @@ -0,0 +1,60 @@ +import libsodium from 'libsodium-wrappers-sumo'; +import { + Protocols, + Ilks, + Serials, + versify, + Vrsn_1_0, +} from '../../src/keri/core/core.ts'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import { Prefixer } from '../../src/keri/core/prefixer.ts'; +import { assert, describe, it } from 'vitest'; + +describe('Prefixer', () => { + it('should create autonomic identifier prefix using derivation as determined by code from ked', async () => { + await libsodium.ready; + + // (b'\xacr\xda\xc83~\x99r\xaf\xeb`\xc0\x8cR\xd7\xd7\xf69\xc8E\x1e\xd2\xf0=`\xf7\xbf\x8a\x18\x8a`q') // from keripy + const verkey = new Uint8Array([ + 172, 114, 218, 200, 51, 126, 153, 114, 175, 235, 96, 192, 140, 82, + 215, 215, 246, 57, 200, 69, 30, 210, 240, 61, 96, 247, 191, 138, 24, + 138, 96, 113, + ]); + let prefixer = new Prefixer({ raw: verkey, code: MtrDex.Ed25519 }); + assert.equal(prefixer.code, MtrDex.Ed25519); + assert.equal( + prefixer.qb64, + 'DKxy2sgzfplyr-tgwIxS19f2OchFHtLwPWD3v4oYimBx' + ); + + // Test digest derivation from inception ked + const vs = versify(Protocols.KERI, Vrsn_1_0, Serials.JSON, 0); + const sn = 0; + const ilk = Ilks.icp; + const sith = '1'; + const keys = [new Prefixer({ raw: verkey, code: MtrDex.Ed25519 }).qb64]; + const nxt = ''; + const toad = 0; + const wits = new Array(); + const cnfg = new Array(); + + const ked = { + v: vs, // version string + i: '', // qb64 prefix + s: sn.toString(16), // hex string no leading zeros lowercase + t: ilk, + kt: sith, // hex string no leading zeros lowercase + k: keys, // list of qb64 + n: nxt, // hash qual Base64 + wt: toad.toString(16), // hex string no leading zeros lowercase + w: wits, // list of qb64 may be empty + c: cnfg, // list of config ordered mappings may be empty + }; + + prefixer = new Prefixer({ code: MtrDex.Blake3_256 }, ked); + assert.equal( + prefixer.qb64, + 'ELEjyRTtmfyp4VpTBTkv_b6KONMS1V8-EW-aGJ5P_QMo' + ); + }); +}); diff --git a/packages/signify-ts/test/core/saider.test.ts b/packages/signify-ts/test/core/saider.test.ts new file mode 100644 index 00000000..4d2494e5 --- /dev/null +++ b/packages/signify-ts/test/core/saider.test.ts @@ -0,0 +1,41 @@ +import { + Protocols, + Serials, + versify, + Vrsn_1_0, +} from '../../src/keri/core/core.ts'; +import { assert, describe, it } from 'vitest'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Saider } from '../../src/keri/core/saider.ts'; + +describe('Saider', () => { + it('should create Saidified dicts', async () => { + await libsodium.ready; + + const kind = Serials.JSON; + const code = MtrDex.Blake3_256; + + const vs = versify(Protocols.KERI, Vrsn_1_0, kind, 0); // vaccuous size == 0 + assert.equal(vs, 'KERI10JSON000000_'); + const sad4 = { + v: vs, + t: 'rep', + d: '', // vacuous said + dt: '2020-08-22T17:50:12.988921+00:00', + r: 'logs/processor', + a: { + d: 'EBabiu_JCkE0GbiglDXNB5C4NQq-hiGgxhHKXBxkiojg', + i: 'EB0_D51cTh_q6uOQ-byFiv5oNXZ-cxdqCqBAa4JmBLtb', + name: 'John Jones', + role: 'Founder', + }, + }; + const saider = new Saider({}, sad4); // default version string code, kind, and label + assert.equal(saider.code, code); + assert.equal( + saider.qb64, + 'ELzewBpZHSENRP-sL_G_2Ji4YDdNkns9AzFzufleJqdw' + ); + }); +}); diff --git a/packages/signify-ts/test/core/salter.test.ts b/packages/signify-ts/test/core/salter.test.ts new file mode 100644 index 00000000..b69ca156 --- /dev/null +++ b/packages/signify-ts/test/core/salter.test.ts @@ -0,0 +1,48 @@ +import { assert, describe, it } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; + +import { Salter } from '../../src/keri/core/salter.ts'; + +describe('Salter', () => { + it('should generate salts', async () => { + await libsodium.ready; + let salter = new Salter({}); + + assert.notEqual(salter, null); + assert.equal(salter.qb64.length, 24); + + const salt = new Uint8Array([ + 146, 78, 142, 186, 189, 77, 130, 3, 232, 248, 186, 197, 8, 0, 73, + 182, + ]); + salter = new Salter({ raw: salt }); + assert.notEqual(salter, null); + assert.equal(salter.qb64, '0ACSTo66vU2CA-j4usUIAEm2'); + + salter = new Salter({ qb64: '0ACSTo66vU2CA-j4usUIAEm2' }); + let raw = new Uint8Array([ + 146, 78, 142, 186, 189, 77, 130, 3, 232, 248, 186, 197, 8, 0, 73, + 182, + ]); + assert.deepStrictEqual(salter.raw, raw); + + salter = new Salter({ qb64: '0ABa4cx6f0SdfwFawI0A7mOZ' }); + raw = new Uint8Array([ + 90, 225, 204, 122, 127, 68, 157, 127, 1, 90, 192, 141, 0, 238, 99, + 153, + ]); + assert.deepStrictEqual(salter.raw, raw); + }); +}); + +describe('Salter.signer', () => { + it('should return a signer', async () => { + const salter = new Salter({ qb64: '0ACSTo66vU2CA-j4usUIAEm2' }); + const signer = salter.signer(); + assert.notEqual(signer, null); + assert.equal( + signer.verfer.qb64, + 'DD28x2a4KCZ8f6OAcA856jAD1chNOo4pT8ICxyzJUJhj' + ); + }); +}); diff --git a/packages/signify-ts/test/core/seqner.test.ts b/packages/signify-ts/test/core/seqner.test.ts new file mode 100644 index 00000000..ac4b6997 --- /dev/null +++ b/packages/signify-ts/test/core/seqner.test.ts @@ -0,0 +1,49 @@ +import { assert, describe, it } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; + +import { Seqner } from '../../src/keri/core/seqner.ts'; + +describe('Seqner', () => { + it('should generate Seqner number class', async () => { + await libsodium.ready; + let seqner = new Seqner({}); + assert.equal(seqner.sn, 0); + assert.equal(seqner.snh, '0'); + assert.equal(seqner.qb64, '0AAAAAAAAAAAAAAAAAAAAAAA'); + assert.notStrictEqual( + seqner.qb64b, + new Uint8Array([ + 48, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, + 65, 65, 65, 65, 65, 65, 65, 65, + ]) + ); + assert.equal(seqner.raw.length, 16); + assert.notStrictEqual( + seqner.raw, + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + ); + + seqner = new Seqner({ snh: '1' }); + assert.equal(seqner.sn, 1); + assert.equal(seqner.snh, '1'); + assert.equal(seqner.qb64, '0AAAAAAAAAAAAAAAAAAAAAAB'); + + seqner = new Seqner({ sn: 1 }); + assert.equal(seqner.sn, 1); + assert.equal(seqner.snh, '1'); + + seqner = new Seqner({ sn: 16 }); + assert.equal(seqner.sn, 16); + assert.equal(seqner.snh, '10'); + assert.equal(seqner.qb64, '0AAAAAAAAAAAAAAAAAAAAAAQ'); + + seqner = new Seqner({ sn: 15 }); + assert.equal(seqner.sn, 15); + assert.equal(seqner.snh, 'f'); + assert.equal(seqner.qb64, '0AAAAAAAAAAAAAAAAAAAAAAP'); + + seqner = new Seqner({ snh: 'f' }); + assert.equal(seqner.sn, 15); + assert.equal(seqner.snh, 'f'); + }); +}); diff --git a/packages/signify-ts/test/core/serder.test.ts b/packages/signify-ts/test/core/serder.test.ts new file mode 100644 index 00000000..75f1d127 --- /dev/null +++ b/packages/signify-ts/test/core/serder.test.ts @@ -0,0 +1,97 @@ +import { deversify, Ilks, Serials, Version } from '../../src/keri/core/core.ts'; +import { assert, describe, it } from 'vitest'; +import { Salter, Tier } from '../../src/keri/core/salter.ts'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import { Diger } from '../../src/keri/core/diger.ts'; +import { Serder } from '../../src/keri/core/serder.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Prefixer } from '../../src/keri/core/prefixer.ts'; + +describe('deversify', () => { + it('should parse a KERI event version string', async () => { + const [, kind, version, size] = deversify('KERI10JSON00011c_'); + assert.equal(kind, Serials.JSON); + assert.deepStrictEqual(version, new Version(1, 0)); + assert.equal(size, '00011c'); + }); +}); + +describe('Serder', () => { + it('should create KERI events from dicts', async () => { + await libsodium.ready; + + const sith = 1; + const nsith = 1; + const sn = 0; + const toad = 0; + + const raw = new Uint8Array([ + 5, 170, 143, 45, 83, 154, 233, 250, 85, 156, 2, 156, 155, 8, 72, + 117, + ]); + const salter = new Salter({ raw: raw }); + const skp0 = salter.signer( + MtrDex.Ed25519_Seed, + true, + 'A', + Tier.low, + true + ); + const keys = [skp0.verfer.qb64]; + + const skp1 = salter.signer( + MtrDex.Ed25519_Seed, + true, + 'N', + Tier.low, + true + ); + const ndiger = new Diger({}, skp1.verfer.qb64b); + const nxt = [ndiger.qb64]; + assert.deepStrictEqual(nxt, [ + 'EAKUR-LmLHWMwXTLWQ1QjxHrihBmwwrV2tYaSG7hOrWj', + ]); + + const ked0 = { + v: 'KERI10JSON000000_', + t: Ilks.icp, + d: '', + i: '', + s: sn.toString(16), + kt: sith.toString(16), + k: keys, + nt: nsith.toString(16), + n: nxt, + bt: toad.toString(16), + b: [], + c: [], + a: [], + }; + + const serder = new Serder(ked0); + assert.equal( + serder.raw, + '{"v":"KERI10JSON0000d3_","t":"icp","d":"","i":"","s":"0","kt":"1","k":' + + '["DAUDqkmn-hqlQKD8W-FAEa5JUvJC2I9yarEem-AAEg3e"],"nt":"1",' + + '"n":["EAKUR-LmLHWMwXTLWQ1QjxHrihBmwwrV2tYaSG7hOrWj"],"bt":"0","b":[],"c":[],"a":[]}' + ); + let aid0 = new Prefixer({ code: MtrDex.Ed25519 }, ked0); + assert.equal(aid0.code, MtrDex.Ed25519); + assert.equal(aid0.qb64, skp0.verfer.qb64); + assert.equal( + skp0.verfer.qb64, + 'DAUDqkmn-hqlQKD8W-FAEa5JUvJC2I9yarEem-AAEg3e' + ); + + aid0 = new Prefixer({ code: MtrDex.Blake3_256 }, ked0); + assert.equal(aid0.qb64, 'ECHOi6qRaswNpvytpCtpvEh2cB2aLAwVHBLFinno3YVW'); + + const serder1 = new Serder({ + ...ked0, + a: { + n: 'Lenksjö', + }, + }); + assert.equal(serder1.sad.v, 'KERI10JSON000139_'); + }); +}); diff --git a/packages/signify-ts/test/core/signer.test.ts b/packages/signify-ts/test/core/signer.test.ts new file mode 100644 index 00000000..2e808d0e --- /dev/null +++ b/packages/signify-ts/test/core/signer.test.ts @@ -0,0 +1,29 @@ +import { assert, describe, it } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; + +import { Signer } from '../../src/index.ts'; +import { Matter, MtrDex } from '../../src/index.ts'; +import { b } from '../../src/index.ts'; + +describe('Signer', () => { + it('should sign things', async () => { + await libsodium.ready; + + const signer = new Signer({}); // defaults provide Ed25519 signer Ed25519 verfer + assert.equal(signer.code, MtrDex.Ed25519_Seed); + assert.equal(signer.raw.length, Matter._rawSize(signer.code)); + assert.equal(signer.verfer.code, MtrDex.Ed25519); + assert.equal( + signer.verfer.raw.length, + Matter._rawSize(signer.verfer.code) + ); + + const ser = b('abcdefghijklmnopqrstuvwxyz0123456789'); + + const cigar = signer.sign(ser); + assert.equal(cigar.code, MtrDex.Ed25519_Sig); + assert.equal(cigar.raw.length, Matter._rawSize(cigar.code)); + const result = signer.verfer.verify(cigar.raw, ser); + assert.equal(result, true); + }); +}); diff --git a/packages/signify-ts/test/core/tholder.test.ts b/packages/signify-ts/test/core/tholder.test.ts new file mode 100644 index 00000000..1ae5be07 --- /dev/null +++ b/packages/signify-ts/test/core/tholder.test.ts @@ -0,0 +1,168 @@ +import { assert, describe, it } from 'vitest'; +import { fraction } from 'mathjs'; +import { Tholder } from '../../src/keri/core/tholder.ts'; + +describe('THolder', () => { + it('should hold thresholds', async () => { + let tholder = new Tholder({ sith: 'b' }); + assert.equal(tholder.thold, 11); + assert.equal(tholder.size, 11); + assert.deepEqual(tholder.limen, new Uint8Array([77, 65, 65, 76])); // b(MAAL) + assert.equal(tholder.sith, 'b'); + assert.equal(tholder.json, '"b"'); + assert.equal(tholder.num, 11); + assert.notEqual(tholder.satisfy([1, 2, 3]), true); + assert.equal( + tholder.satisfy([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + true + ); + + tholder = new Tholder({ sith: 11 }); + assert.equal(tholder.thold, 11); + assert.equal(tholder.size, 11); + assert.deepEqual(tholder.limen, new Uint8Array([77, 65, 65, 76])); // b(MAAL) + assert.equal(tholder.sith, 'b'); + assert.equal(tholder.json, '"b"'); + assert.equal(tholder.num, 11); + assert.notEqual(tholder.satisfy([1, 2, 3]), true); + assert.equal( + tholder.satisfy([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + true + ); + + tholder = new Tholder({ thold: 2 }); + assert.equal(tholder.thold, 2); + assert.equal(tholder.size, 2); + assert.deepEqual(tholder.limen, new Uint8Array([77, 65, 65, 67])); // b(MAAI) + assert.equal(tholder.sith, '2'); + assert.equal(tholder.json, '"2"'); + assert.equal(tholder.num, 2); + assert.notEqual(tholder.satisfy([1]), true); + assert.equal(tholder.satisfy([1, 2]), true); + assert.equal(tholder.satisfy([1, 2, 3, 4]), true); + + assert.throws(() => { + new Tholder({ sith: -1 }); + }); + + assert.throws(() => { + tholder = new Tholder({ + sith: ['1/2', '1/2', ['1/3', '1/3', '1/3']], + }); + }); + + assert.throws(() => { + tholder = new Tholder({ + sith: [ + ['1/2', '1/2'], + ['1/4', '1/4', '1/4'], + ], + }); + }); + + // math.fractional Weights + tholder = new Tholder({ sith: ['1/2', '1/2', '1/4', '1/4', '1/4'] }); + assert.equal(tholder.weighted, true); + assert.equal(tholder.size, 5); + assert.deepStrictEqual(tholder.thold, [ + [ + fraction('1/2'), + fraction('1/2'), + fraction('1/4'), + fraction('1/4'), + fraction('1/4'), + ], + ]); + assert.equal(tholder.satisfy([0, 1]), true); + assert.equal(tholder.satisfy([0, 2, 4]), true); + assert.equal(tholder.satisfy([1, 3, 4]), true); + assert.equal(tholder.satisfy([0, 1, 2, 3, 4]), true); + assert.equal(tholder.satisfy([0, 2, 3]), true); + assert.equal(tholder.satisfy([0, 0, 1, 2, 1]), true); + assert.notEqual(tholder.satisfy([0]), true); + assert.notEqual(tholder.satisfy([0, 2]), true); + assert.notEqual(tholder.satisfy([2, 3, 4]), true); + + tholder = new Tholder({ + sith: [ + ['1/2', '1/2', '1/2'], + ['1/3', '1/3', '1/3', '1/3'], + ], + }); + assert.equal(tholder.weighted, true); + assert.equal(tholder.size, 7); + assert.deepStrictEqual(tholder.sith, [ + ['1/2', '1/2', '1/2'], + ['1/3', '1/3', '1/3', '1/3'], + ]); + assert.deepStrictEqual(tholder.thold, [ + [fraction(1, 2), fraction(1, 2), fraction(1, 2)], + [fraction(1, 3), fraction(1, 3), fraction(1, 3), fraction(1, 3)], + ]); + assert.equal(tholder.satisfy([0, 2, 3, 5, 6]), true); + assert.equal(tholder.satisfy([1, 2, 3, 4, 5]), true); + assert.notEqual(tholder.satisfy([0, 1]), true); + assert.notEqual(tholder.satisfy([0, 2]), true); + assert.notEqual(tholder.satisfy([4, 5, 6]), true); + assert.notEqual(tholder.satisfy([1, 4, 5, 6]), true); + + tholder = new Tholder({ + sith: '[["1/2", "1/2", "1/4", "1/4", "1/4"], ["1/1", "1"]]', + }); + assert.equal(tholder.weighted, true); + assert.equal(tholder.size, 7); + assert.deepStrictEqual(tholder.sith, [ + ['1/2', '1/2', '1/4', '1/4', '1/4'], + ['1', '1'], + ]); + assert.deepStrictEqual(tholder.thold, [ + [ + fraction(1, 2), + fraction(1, 2), + fraction(1, 4), + fraction(1, 4), + fraction(1, 4), + ], + [fraction(1, 1), fraction(1, 1)], + ]); + assert.equal(tholder.satisfy([1, 2, 3, 5]), true); + assert.equal(tholder.satisfy([0, 1, 6]), true); + assert.notEqual(tholder.satisfy([0, 1]), true); + assert.notEqual(tholder.satisfy([5, 6]), true); + assert.notEqual(tholder.satisfy([2, 3, 4]), true); + assert.notEqual(tholder.satisfy([]), true); + + tholder = new Tholder({ + sith: [ + ['1/2', '1/2', '1/4', '1/4', '1/4'], + ['1/1', '1'], + ], + }); + assert.equal(tholder.weighted, true); + assert.equal(tholder.size, 7); + assert.deepStrictEqual(tholder.sith, [ + ['1/2', '1/2', '1/4', '1/4', '1/4'], + ['1', '1'], + ]); + assert.deepStrictEqual( + tholder.json, + '[["1/2","1/2","1/4","1/4","1/4"],["1","1"]]' + ); + assert.deepStrictEqual(tholder.thold, [ + [ + fraction(1, 2), + fraction(1, 2), + fraction(1, 4), + fraction(1, 4), + fraction(1, 4), + ], + [fraction(1, 1), fraction(1, 1)], + ]); + assert.equal(tholder.satisfy([1, 2, 3, 5]), true); + assert.equal(tholder.satisfy([0, 1, 6]), true); + assert.notEqual(tholder.satisfy([0, 1]), true); + assert.notEqual(tholder.satisfy([5, 6]), true); + assert.notEqual(tholder.satisfy([2, 3, 4]), true); + assert.notEqual(tholder.satisfy([]), true); + }); +}); diff --git a/packages/signify-ts/test/core/utils.test.ts b/packages/signify-ts/test/core/utils.test.ts new file mode 100644 index 00000000..1a4de243 --- /dev/null +++ b/packages/signify-ts/test/core/utils.test.ts @@ -0,0 +1,49 @@ +import { + Protocols, + Saider, + Serder, + Serials, + d, + versify, +} from '../../src/index.ts'; +import { + serializeACDCAttachment, + serializeIssExnAttachment, +} from '../../src/keri/core/utils.ts'; +import { describe, it, assert } from 'vitest'; + +describe(serializeIssExnAttachment.name, () => { + it('serializes iss data', () => { + const [, data] = Saider.saidify({ + d: '', + v: versify(Protocols.KERI, undefined, Serials.JSON, 0), + }); + + const result = serializeIssExnAttachment(new Serder(data)); + + assert.equal( + d(result), + '-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAAAEKZPmzJqhx76bcC2ftPQgeRirmOd8ZBOtGVqHJrSm7F1' + ); + }); +}); + +describe(serializeACDCAttachment.name, () => { + it('serializes acdc data', () => { + const [, data] = Saider.saidify({ + i: 'EP-hA0w9X5FDonCDxQv32OTCAvcxkZxgDLOnDb3Jcn3a', + d: '', + v: versify(Protocols.ACDC, undefined, Serials.JSON, 0), + a: { + LEI: '123', + }, + }); + + const result = serializeACDCAttachment(new Serder(data)); + + assert.equal( + d(result), + '-IABEP-hA0w9X5FDonCDxQv32OTCAvcxkZxgDLOnDb3Jcn3a0AAAAAAAAAAAAAAAAAAAAAAAEHGU7u7cSMjMcJ1UyN8r-MnoZ3cDw4sMQNYxRLjqGVJI' + ); + }); +}); diff --git a/packages/signify-ts/test/core/vdring.test.ts b/packages/signify-ts/test/core/vdring.test.ts new file mode 100644 index 00000000..be83defb --- /dev/null +++ b/packages/signify-ts/test/core/vdring.test.ts @@ -0,0 +1,128 @@ +import libsodium from 'libsodium-wrappers-sumo'; +import { vdr } from '../../src/keri/core/vdring.ts'; +import { assert, describe, it } from 'vitest'; + +describe('vdr', () => { + it('should create registry inception events ', async () => { + await libsodium.ready; + let actual = vdr.incept({ + pre: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + toad: 0, + }); + assert.equal(actual.pre.length, 44); + + actual = vdr.incept({ + pre: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + toad: 0, + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + }); + assert.equal( + actual.pre, + 'EDAsrwU75uoh8sii7w-KN-Txy2d0dhHiUP34TQVBJiPS' + ); + assert.equal(actual.code, 'E'); + assert.equal( + actual.raw, + '{"v":"KERI10JSON00010f_","t":"vcp","d":"EDAsrwU75uoh8sii7w-KN-Txy2d0dhHiUP34TQVBJiPS","i":"EDAsrwU75uoh8sii7w-KN-Txy2d0dhHiUP34TQVBJiPS","ii":"ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g","s":"0","c":[],"bt":"0","b":[],"n":"AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s"}' + ); + assert.equal(actual.size, 271); + }); + + it('should fail on NB config with backers', async () => { + await libsodium.ready; + const cnfg = ['NB']; + assert.throws(() => { + vdr.incept({ + pre: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + toad: 0, + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + cnfg: cnfg, + baks: ['a backer'], + }); + }, '1 backers specified for NB vcp, 0 allowed'); + }); + + it('should fail with duplicate backers', async () => { + await libsodium.ready; + assert.throws(() => { + vdr.incept({ + pre: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + toad: 0, + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + baks: ['a backer', 'a backer'], + }); + }, 'Invalid baks a backer,a backer has duplicates'); + }); + + it('should fail with invalid toad config for backers', async () => { + await libsodium.ready; + assert.throws(() => { + vdr.incept({ + pre: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + toad: 0, + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + baks: ['a backer'], + }); + }, 'Invalid toad 0 for baks in a backer'); + }); + + it('should fail with invalid toad for no backers', async () => { + await libsodium.ready; + assert.throws(() => { + vdr.incept({ + pre: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + toad: 1, + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + }); + }, 'Invalid toad 1 for no baks'); + }); + + it('should allow optional toad and no backers', async () => { + await libsodium.ready; + const actual = vdr.incept({ + pre: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + }); + + assert.equal( + actual.pre, + 'EDAsrwU75uoh8sii7w-KN-Txy2d0dhHiUP34TQVBJiPS' + ); + assert.equal(actual.code, 'E'); + assert.equal( + actual.raw, + '{"v":"KERI10JSON00010f_","t":"vcp","d":"EDAsrwU75uoh8sii7w-KN-Txy2d0dhHiUP34TQVBJiPS","i":"EDAsrwU75uoh8sii7w-KN-Txy2d0dhHiUP34TQVBJiPS","ii":"ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g","s":"0","c":[],"bt":"0","b":[],"n":"AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s"}' + ); + assert.equal(actual.size, 271); + }); + + it('should allow optional toad and backers', async () => { + await libsodium.ready; + const actual = vdr.incept({ + pre: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + nonce: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + baks: ['a backer'], + toad: 1, + }); + + const expectedPrefix = 'ENlghG6_krj9YMzy5-E3j5sEjsd6FR1nskBtbtSQGOFL'; + assert.equal(actual.pre, expectedPrefix); + assert.equal(actual.code, 'E'); + assert.equal( + actual.raw, + JSON.stringify({ + v: 'KERI10JSON000119_', + t: 'vcp', + d: expectedPrefix, + i: expectedPrefix, + ii: 'ECJIoBpEcCWMzvquk861dXP8JJZ-vbmJczlDR-NYcE3g', + s: '0', + c: [], + bt: '1', + b: ['a backer'], + n: 'AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s', + }) + ); + assert.equal(actual.size, 281); + }); +}); diff --git a/packages/signify-ts/test/core/verfer.test.ts b/packages/signify-ts/test/core/verfer.test.ts new file mode 100644 index 00000000..2d4d09c0 --- /dev/null +++ b/packages/signify-ts/test/core/verfer.test.ts @@ -0,0 +1,131 @@ +import { MtrDex } from '../../src/keri/core/matter.ts'; +import libsodium from 'libsodium-wrappers-sumo'; +import { assert, describe, it, expect, beforeAll } from 'vitest'; +import { Verfer } from '../../src/keri/core/verfer.ts'; +import { p256 } from '@noble/curves/p256'; +import { b } from 'signify-ts'; + +beforeAll(async () => { + await libsodium.ready; +}); + +describe('Verfer', () => { + describe('Ed25519', () => { + const seed = libsodium.randombytes_buf(libsodium.crypto_sign_SEEDBYTES); + const keypair = libsodium.crypto_sign_seed_keypair(seed); + + it('should create verfer', async () => { + const verkey = keypair.publicKey; + + const verfer = new Verfer({ + raw: keypair.publicKey, + code: MtrDex.Ed25519N, + }); + + assert.deepStrictEqual(verfer.raw, verkey); + assert.deepStrictEqual(verfer.code, MtrDex.Ed25519N); + }); + + it('should verify signature', () => { + const verfer = new Verfer({ + raw: keypair.publicKey, + code: MtrDex.Ed25519N, + }); + const ser = 'abcdefghijklmnopqrstuvwxyz0123456789'; + + const sig = libsodium.crypto_sign_detached(ser, keypair.privateKey); + + assert.equal(verfer.verify(sig, ser), true); + }); + + it('should create verfer from qb64', async () => { + const verfer = new Verfer({ + qb64: 'BGgVB5Aar1pOr70nRpJmRA_RP68HErflNovoEMP7b7mJ', + }); + + assert.deepStrictEqual( + verfer.raw, + new Uint8Array([ + 104, 21, 7, 144, 26, 175, 90, 78, 175, 189, 39, 70, 146, + 102, 68, 15, 209, 63, 175, 7, 18, 183, 229, 54, 139, 232, + 16, 195, 251, 111, 185, 137, + ]) + ); + }); + }); + + describe('secp256r1', () => { + const privateKey = new Uint8Array([ + 138, 17, 14, 173, 86, 68, 80, 39, 61, 52, 208, 154, 211, 190, 21, + 99, 156, 134, 184, 90, 166, 171, 226, 251, 239, 132, 127, 221, 144, + 51, 245, 71, + ]); + + const publicKey = p256.getPublicKey(privateKey); + + it('should create verfer', async () => { + const verfer = new Verfer({ + raw: publicKey, + code: MtrDex.ECDSA_256r1, + }); + + assert.deepStrictEqual(verfer.raw, publicKey); + assert.deepStrictEqual(verfer.code, MtrDex.ECDSA_256r1); + assert.equal( + verfer.qb64, + '1AAJA-blKBTkTkEEOX_Yq3i3KxZJvcHarPfu_crKVwcfEwvQ' + ); + }); + + it('should verify secp256r1', async () => { + const verfer = new Verfer({ + raw: publicKey, + code: MtrDex.ECDSA_256r1, + }); + + const ser = 'abcdefghijklmnopqrstuvwxyz0123456789'; + + const sig = p256.sign(b(ser), privateKey).toCompactRawBytes(); + + assert.deepEqual( + sig, + new Uint8Array([ + 56, 81, 180, 93, 192, 44, 174, 128, 161, 173, 226, 227, 149, + 26, 203, 255, 36, 189, 144, 110, 163, 51, 67, 138, 99, 130, + 38, 189, 16, 170, 164, 77, 167, 254, 123, 220, 149, 72, 71, + 28, 32, 66, 213, 177, 197, 158, 195, 234, 167, 109, 207, + 174, 15, 221, 245, 85, 120, 226, 224, 33, 94, 89, 115, 49, + ]) + ); + + assert.equal(verfer.verify(sig, ser), true); + }); + + it('should parse qb64', () => { + const verfer = new Verfer({ + qb64: '1AAJAwf0oSqmdjPud5gnK6bAPKkBLrXUMQZiOW4Vpc4XpOPf', + }); + + assert.deepStrictEqual( + verfer.raw, + new Uint8Array([ + 3, 7, 244, 161, 42, 166, 118, 51, 238, 119, 152, 39, 43, + 166, 192, 60, 169, 1, 46, 181, 212, 49, 6, 98, 57, 110, 21, + 165, 206, 23, 164, 227, 223, + ]) + ); + }); + }); + + it('should not verify secp256k1', async () => { + const publicKey = new Uint8Array([ + 2, 79, 93, 30, 107, 249, 254, 237, 205, 87, 8, 149, 203, 214, 36, + 187, 162, 251, 58, 206, 241, 203, 27, 76, 236, 37, 189, 148, 240, + 178, 204, 133, 31, + ]); + + expect(() => { + new Verfer({ raw: publicKey, code: MtrDex.ECDSA_256k1 }); + }).toThrow(new Error(`Unsupported code = 1AAB for verifier.`)); + }); +}); diff --git a/packages/signify-ts/test/end/ending.test.ts b/packages/signify-ts/test/end/ending.test.ts new file mode 100644 index 00000000..c9f31bf3 --- /dev/null +++ b/packages/signify-ts/test/end/ending.test.ts @@ -0,0 +1,260 @@ +import { assert, describe, it, beforeAll, beforeEach } from 'vitest'; +import libsodium from 'libsodium-wrappers-sumo'; +import { Salter, Tier } from '../../src/keri/core/salter.ts'; +import { b } from '../../src/keri/core/core.ts'; +import { MtrDex } from '../../src/keri/core/matter.ts'; +import { designature, Signage, signature } from '../../src/keri/end/ending.ts'; +import { Siger } from '../../src/keri/core/siger.ts'; +import { Cigar } from '../../src/keri/core/cigar.ts'; +import { Signer } from '../../src/keri/core/signer.ts'; + +function createSigner(name: string): Signer { + const temp = true; + + const salter = new Salter({ raw: b('0123456789abcdef') }); + const signer0 = salter.signer( + MtrDex.Ed25519_Seed, + true, + name, + Tier.low, + temp + ); + + return signer0; +} + +let sigers: Siger[]; +let cigars: Cigar[]; +let text: Uint8Array; +let pre: string; +let digest: string; + +beforeAll(async () => { + await libsodium.ready; + + const name = 'Hilga'; + const signer0 = createSigner(`${name}00`); + const signer1 = createSigner(`${name}01`); + const signer2 = createSigner(`${name}02`); + const signers = [signer0, signer1, signer2]; + text = b( + JSON.stringify({ + seid: 'BA89hKezugU2LFKiFVbitoHAxXqJh6HQ8Rn9tH7fxd68', + name: 'wit0', + dts: '2021-01-01T00:00:00.000000+00:00', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/witness', + }) + ); + sigers = signers.map((signer, idx) => signer.sign(text, idx) as Siger); + cigars = signers.map((s) => s.sign(text) as Cigar); + pre = 'EGqHykT1gVyuWxsVW6LUUsz_KtLJGYMi_SrohInwvjC-'; // Hab.pre from KERIpy test + digest = pre; +}); + +describe('When indexed signatures', () => { + const expectedHeader = [ + 'indexed="?1"', + '0="AACsufRGYI-sRvS2c0rsOueSoSRtrjODaf48DYLJbLvvD8aHe7b2sWGebZ-y9ichhsxMF3Hhn-3LYSKIrnmH3oIN"', + '1="ABDs7m2-h5l7vpjYtbFXtksicpZK5Oclm43EOkE2xoQOfr08doj73VrlKZOKNfJmRumD3tfaiFFgVZqPgiHuFVoA"', + '2="ACDVOy2LvGgFINUneL4iwA55ypJR6vDpLLbdleEsiANmFazwZARypJMiw9vu2Iu0oL7XCUiUT4JncU8P3HdIp40F"', + ].join(';'); + + it('Can create signature header', async () => { + const signage = new Signage(sigers); + const header = signature([signage]); + assert.equal(header.has('Signature'), true); + assert.equal(header.get('Signature'), expectedHeader); + }); + + it('Can parse signature header', async () => { + const signages = designature(expectedHeader); + assert.equal(signages.length, 1); + const signage = signages[0]; + + assert(signage.markers instanceof Map); + assert.equal(signage.markers.size, 3); + signage.markers.forEach((marker, tag) => { + assert(marker instanceof Siger); + const idx = parseInt(tag); + const siger = sigers[idx]; + assert.equal(marker.qb64, siger.qb64); + assert.equal(parseInt(tag), siger.index); + }); + }); +}); + +describe('When named signatures', () => { + const expectedHeader = [ + 'indexed="?1"', + 'siger0="AACsufRGYI-sRvS2c0rsOueSoSRtrjODaf48DYLJbLvvD8aHe7b2sWGebZ-y9ichhsxMF3Hhn-3LYSKIrnmH3oIN"', + 'siger1="ABDs7m2-h5l7vpjYtbFXtksicpZK5Oclm43EOkE2xoQOfr08doj73VrlKZOKNfJmRumD3tfaiFFgVZqPgiHuFVoA"', + 'siger2="ACDVOy2LvGgFINUneL4iwA55ypJR6vDpLLbdleEsiANmFazwZARypJMiw9vu2Iu0oL7XCUiUT4JncU8P3HdIp40F"', + ].join(';'); + + let markers: Map; + + beforeEach(() => { + markers = new Map( + sigers.map((s, idx) => [`siger${idx}`, s]) + ); + }); + + it('Can create signature header', async () => { + const signage = new Signage(markers); + const header = signature([signage]); + assert.equal(header.has('Signature'), true); + assert.equal(header.get('Signature'), expectedHeader); + }); + + it('Can parse signature header', async () => { + const signages = designature(expectedHeader); + assert.equal(signages.length, 1); + const signage = signages[0]; + + assert(signage.markers instanceof Map); + assert.equal(signage.markers.size, 3); + signage.markers.forEach((marker, tag) => { + const siger = markers.get(tag); + + assert(marker instanceof Siger); + assert(siger); + assert.equal(marker.qb64, siger.qb64); + }); + }); +}); + +describe('When indexed CESR signatures', () => { + const expectedHeader = [ + 'indexed="?1"', + 'signer="EGqHykT1gVyuWxsVW6LUUsz_KtLJGYMi_SrohInwvjC-"', + 'ordinal="0"', + 'digest="EGqHykT1gVyuWxsVW6LUUsz_KtLJGYMi_SrohInwvjC-"', + 'kind="CESR"', + '0="AACsufRGYI-sRvS2c0rsOueSoSRtrjODaf48DYLJbLvvD8aHe7b2sWGebZ-y9ichhsxMF3Hhn-3LYSKIrnmH3oIN"', + '1="ABDs7m2-h5l7vpjYtbFXtksicpZK5Oclm43EOkE2xoQOfr08doj73VrlKZOKNfJmRumD3tfaiFFgVZqPgiHuFVoA"', + '2="ACDVOy2LvGgFINUneL4iwA55ypJR6vDpLLbdleEsiANmFazwZARypJMiw9vu2Iu0oL7XCUiUT4JncU8P3HdIp40F"', + ].join(';'); + + it('Should create headers', async () => { + const signage = new Signage(sigers, true, pre, '0', digest, 'CESR'); + const headers = signature([signage]); + assert.equal(headers.has('Signature'), true); + assert.equal(headers.get('Signature'), expectedHeader); + }); + + it('Should parse headers', async () => { + const signages = designature(expectedHeader); + assert.equal(signages.length, 1); + const signage = signages[0]; + assert.equal(signage.indexed, true); + assert.equal(signage.signer, pre); + assert.equal(signage.digest, digest); + assert.equal(signage.kind, 'CESR'); + + assert(signage.markers instanceof Map); + assert.equal(signage.markers.size, 3); + signage.markers.forEach((marker, tag) => { + assert(marker instanceof Siger); + const idx = parseInt(tag); + const siger = sigers[idx]; + assert.equal(marker.qb64, siger.qb64); + assert.equal(parseInt(tag), siger.index); + }); + }); +}); + +describe('When non-indexed signatures', () => { + const expectedHeader = [ + 'indexed="?0"', + 'DAi2TaRNVtGmV8eSUvqHIBzTzIgrQi57vKzw5Svmy7jw="0BCsufRGYI-sRvS2c0rsOueSoSRtrjODaf48DYLJbLvvD8aHe7b2sWGebZ-y9ichhsxMF3Hhn-3LYSKIrnmH3oIN"', + 'DNK2KFnL0jUGlmvZHRse7HwNGVdtkM-ORvTZfFw7mDbt="0BDs7m2-h5l7vpjYtbFXtksicpZK5Oclm43EOkE2xoQOfr08doj73VrlKZOKNfJmRumD3tfaiFFgVZqPgiHuFVoA"', + 'DDvIoIYqeuXJ4Zb8e2luWfjPTg4FeIzfHzIO8lC56WjD="0BDVOy2LvGgFINUneL4iwA55ypJR6vDpLLbdleEsiANmFazwZARypJMiw9vu2Iu0oL7XCUiUT4JncU8P3HdIp40F"', + ].join(';'); + + it('Should create headers', () => { + const signage = new Signage(cigars); + const header = signature([signage]); + assert.equal(header.has('Signature'), true); + assert.equal(header.get('Signature'), expectedHeader); + }); + + it('Should parse headers', () => { + const signages = designature(expectedHeader); + assert.equal(signages.length, 1); + const signage = signages[0]; + assert.equal(signage.indexed, false); + assert(signage.markers instanceof Map); + assert.equal(signage.markers.size, 3); + + signage.markers.forEach((marker, tag) => { + assert(marker instanceof Cigar); + const cigar = cigars.find((cigar) => cigar.verfer!.qb64 == tag); + assert.notEqual(cigar, undefined); + assert.equal(marker.qb64, cigar!.qb64); + assert.equal(tag, cigar!.verfer!.qb64); + }); + }); +}); + +describe('Combined headers', () => { + const expectedHeader = [ + 'indexed="?1"', + 'signer="EGqHykT1gVyuWxsVW6LUUsz_KtLJGYMi_SrohInwvjC-"', + 'kind="CESR"', + '0="AACsufRGYI-sRvS2c0rsOueSoSRtrjODaf48DYLJbLvvD8aHe7b2sWGebZ-y9ichhsxMF3Hhn-3LYSKIrnmH3oIN"', + '1="ABDs7m2-h5l7vpjYtbFXtksicpZK5Oclm43EOkE2xoQOfr08doj73VrlKZOKNfJmRumD3tfaiFFgVZqPgiHuFVoA"', + '2="ACDVOy2LvGgFINUneL4iwA55ypJR6vDpLLbdleEsiANmFazwZARypJMiw9vu2Iu0oL7XCUiUT4JncU8P3HdIp40F",indexed="?0"', + 'signer="EGqHykT1gVyuWxsVW6LUUsz_KtLJGYMi_SrohInwvjC-"', + 'kind="CESR"', + 'DAi2TaRNVtGmV8eSUvqHIBzTzIgrQi57vKzw5Svmy7jw="0BCsufRGYI-sRvS2c0rsOueSoSRtrjODaf48DYLJbLvvD8aHe7b2sWGebZ-y9ichhsxMF3Hhn-3LYSKIrnmH3oIN"', + 'DNK2KFnL0jUGlmvZHRse7HwNGVdtkM-ORvTZfFw7mDbt="0BDs7m2-h5l7vpjYtbFXtksicpZK5Oclm43EOkE2xoQOfr08doj73VrlKZOKNfJmRumD3tfaiFFgVZqPgiHuFVoA"', + 'DDvIoIYqeuXJ4Zb8e2luWfjPTg4FeIzfHzIO8lC56WjD="0BDVOy2LvGgFINUneL4iwA55ypJR6vDpLLbdleEsiANmFazwZARypJMiw9vu2Iu0oL7XCUiUT4JncU8P3HdIp40F"', + ].join(';'); + + it('Should create header', () => { + const signages: Signage[] = [ + new Signage(sigers, true, pre, undefined, undefined, 'CESR'), + new Signage(cigars, false, pre, undefined, undefined, 'CESR'), + ]; + + const header = signature(signages); + assert.equal(header.has('Signature'), true); + assert.equal(header.get('Signature'), expectedHeader); + }); + + it('Should parse hader', () => { + const signages = designature(expectedHeader); + assert.equal(signages.length, 2); + + const signage0 = signages[0]; + assert.equal(signage0.indexed, true); + assert.equal(signage0.signer, pre); + assert.equal(signage0.kind, 'CESR'); + assert(signage0.markers instanceof Map); + assert.equal(signage0.markers.size, 3); + signage0.markers.forEach((marker, tag) => { + assert(marker instanceof Siger); + const idx = parseInt(tag); + const siger = sigers[idx]; + assert.equal(marker.qb64, siger.qb64); + assert.equal(parseInt(tag), siger.index); + }); + + const signage1 = signages[1]; + assert.equal(signage1.indexed, false); + assert.equal(signage1.signer, pre); + assert.equal(signage1.kind, 'CESR'); + assert(signage1.markers instanceof Map); + assert.equal(signage1.markers.size, 3); + signage1.markers.forEach((marker, tag) => { + assert(marker instanceof Cigar); + const cigar = cigars.find((cigar) => cigar.verfer!.qb64 == tag); + assert.notEqual(cigar, undefined); + assert.equal(marker.qb64, cigar!.qb64); + assert.equal(tag, cigar!.verfer!.qb64); + }); + }); +}); diff --git a/packages/signify-ts/tsconfig.build.json b/packages/signify-ts/tsconfig.build.json new file mode 100644 index 00000000..ac48a7ac --- /dev/null +++ b/packages/signify-ts/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "include": ["src", "types.d.ts"], + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "noEmit": false + } +} diff --git a/packages/signify-ts/tsconfig.json b/packages/signify-ts/tsconfig.json new file mode 100644 index 00000000..e9dcee17 --- /dev/null +++ b/packages/signify-ts/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["src", "test", "test-integration"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "noEmit": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "declaration": true, + "target": "ES2017", + "module": "Node16", + "lib": ["dom", "esnext"], + "moduleResolution": "node16", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "paths": { + "signify-ts": ["./src/index.ts"] + } + } +} diff --git a/packages/signify-ts/vitest.config.ts b/packages/signify-ts/vitest.config.ts new file mode 100644 index 00000000..b3861457 --- /dev/null +++ b/packages/signify-ts/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + name: 'Unit tests', + root: 'test', + testTimeout: 10000, + }, +}); diff --git a/packages/signify-ts/vitest.integration.ts b/packages/signify-ts/vitest.integration.ts new file mode 100644 index 00000000..2c37267d --- /dev/null +++ b/packages/signify-ts/vitest.integration.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; +import tsconfig from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [tsconfig()], + test: { + fileParallelism: false, + name: 'Integration tests', + root: 'test-integration', + bail: 1, + testTimeout: 60000, + watch: false, + }, +}); diff --git a/src/keria/app/ipexing.py b/src/keria/app/ipexing.py index 944a50ea..a91e1062 100644 --- a/src/keria/app/ipexing.py +++ b/src/keria/app/ipexing.py @@ -106,7 +106,7 @@ def sendAdmit(agent, hab, ked, sigs, rec): ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) # now get rid of the event so we can pass it as atc to send del ims[: serder.size] @@ -160,7 +160,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): ims.extend(atc.encode("utf-8")) # add the pathed attachments # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) if not exn: @@ -180,7 +180,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): serder = serdering.SerderKERI(sad=admitked) ims = bytearray(serder.raw) + pathed["exn"] - agent.parser.parseOne(ims=ims) + agent.hby.psr.parseOne(ims=ims) agent.exchanges.append( dict( said=serder.said, @@ -271,7 +271,7 @@ def sendGrant(agent, hab, ked, sigs, atc, rec): ims = ims + atc.encode("utf-8") # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) # now get rid of the event so we can pass it as atc to send del ims[: serder.size] @@ -325,7 +325,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): ims.extend(atc.encode("utf-8")) # add the pathed attachments # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) if not exn: @@ -340,7 +340,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): grantRec = grant["a"]["i"] serder = serdering.SerderKERI(sad=grant) ims = bytearray(serder.raw) + pathed["exn"] - agent.parser.parseOne(ims=ims) + agent.hby.psr.parseOne(ims=ims) agent.exchanges.append( dict(said=serder.said, pre=hab.pre, rec=[grantRec], topic="credential") ) @@ -428,7 +428,7 @@ def sendApply(agent, hab, ked, sigs, rec): ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) agent.exchanges.append( dict(said=serder.said, pre=hab.pre, rec=rec, topic="credential") @@ -476,7 +476,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): ims.extend(atc.encode("utf-8")) # add the pathed attachments # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) if not exn: raise falcon.HTTPBadRequest( @@ -490,7 +490,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): applyRec = applyked["a"]["i"] serder = serdering.SerderKERI(sad=applyked) ims = bytearray(serder.raw) + pathed["exn"] - agent.parser.parseOne(ims=ims) + agent.hby.psr.parseOne(ims=ims) agent.exchanges.append( dict(said=serder.said, pre=hab.pre, rec=[applyRec], topic="credential") ) @@ -575,7 +575,7 @@ def sendOffer(agent, hab, ked, sigs, atc, rec): ims = ims + atc.encode("utf-8") # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) agent.exchanges.append( dict(said=serder.said, pre=hab.pre, rec=rec, topic="credential") @@ -623,7 +623,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): ims.extend(atc.encode("utf-8")) # add the pathed attachments # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) if not exn: raise falcon.HTTPBadRequest( @@ -643,7 +643,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): offerRec = offerked["a"]["i"] serder = serdering.SerderKERI(sad=offerked) ims = bytearray(serder.raw) + pathed["exn"] - agent.parser.parseOne(ims=ims) + agent.hby.psr.parseOne(ims=ims) agent.exchanges.append( dict(said=serder.said, pre=hab.pre, rec=[offerRec], topic="credential") ) @@ -727,7 +727,7 @@ def sendAgree(agent, hab, ked, sigs, rec): ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) agent.exchanges.append( dict(said=serder.said, pre=hab.pre, rec=rec, topic="credential") @@ -775,7 +775,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): ims.extend(atc.encode("utf-8")) # add the pathed attachments # make a copy and parse - agent.parser.parseOne(ims=bytearray(ims)) + agent.hby.psr.parseOne(ims=bytearray(ims)) exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) if not exn: raise falcon.HTTPBadRequest( @@ -795,7 +795,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): agreeRec = agreeKed["a"]["i"] serder = serdering.SerderKERI(sad=agreeKed) ims = bytearray(serder.raw) + pathed["exn"] - agent.parser.parseOne(ims=ims) + agent.hby.psr.parseOne(ims=ims) agent.exchanges.append( dict(said=serder.said, pre=hab.pre, rec=[agreeRec], topic="credential") ) diff --git a/version.sh b/version.sh new file mode 100755 index 00000000..0153b683 --- /dev/null +++ b/version.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +tag=${RELEASE_TAG:-dev} +version=$(cat pyproject.toml | grep "version =" | cut -d= -f2 | tr -d '" ') + +if ! [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Version does not match semver spec: $version" + exit 1 +fi + +if [ "$tag" = "dev" ]; then + version="${version}-dev.$(git rev-parse --short HEAD)" +fi + +echo "$version"